Skip to content

Commit 6049acb

Browse files
committed
Lint small gaps between ranges
1 parent 87e93f6 commit 6049acb

File tree

16 files changed

+467
-102
lines changed

16 files changed

+467
-102
lines changed

compiler/rustc_lint_defs/src/builtin.rs

+31
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ declare_lint_pass! {
8787
RUST_2021_PRELUDE_COLLISIONS,
8888
SEMICOLON_IN_EXPRESSIONS_FROM_MACROS,
8989
SINGLE_USE_LIFETIMES,
90+
SMALL_GAPS_BETWEEN_RANGES,
9091
SOFT_UNSTABLE,
9192
STABLE_FEATURES,
9293
SUSPICIOUS_AUTO_TRAIT_IMPLS,
@@ -835,6 +836,36 @@ declare_lint! {
835836
"detects range patterns with overlapping endpoints"
836837
}
837838

839+
declare_lint! {
840+
/// The `small_gaps_between_ranges` lint detects `match` expressions that use [range patterns]
841+
/// that skip over a single number.
842+
///
843+
/// [range patterns]: https://doc.rust-lang.org/nightly/reference/patterns.html#range-patterns
844+
///
845+
/// ### Example
846+
///
847+
/// ```rust
848+
/// # #![feature(exclusive_range_pattern)]
849+
/// let x = 123u32;
850+
/// match x {
851+
/// 0..100 => { println!("small"); }
852+
/// 101..1000 => { println!("large"); }
853+
/// _ => { println!("larger"); }
854+
/// }
855+
/// ```
856+
///
857+
/// {{produces}}
858+
///
859+
/// ### Explanation
860+
///
861+
/// It is likely a mistake to have range patterns in a match expression that miss out a single
862+
/// number. Check that the beginning and end values are what you expect, and keep in mind that
863+
/// with `..=` the right bound is inclusive, and with `..` it is exclusive.
864+
pub SMALL_GAPS_BETWEEN_RANGES,
865+
Warn,
866+
"detects range patterns separated by a single number"
867+
}
868+
838869
declare_lint! {
839870
/// The `bindings_with_variant_name` lint detects pattern bindings with
840871
/// the same name as one of the matched variants.

compiler/rustc_pattern_analysis/messages.ftl

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ pattern_analysis_overlapping_range_endpoints = multiple patterns overlap on thei
1111
.label = ... with this range
1212
.note = you likely meant to write mutually exclusive ranges
1313
14+
pattern_analysis_small_gap_between_ranges = multiple ranges are one apart
15+
.label = this range doesn't match `{$gap}` because `..` is a non-inclusive range
16+
.suggestion = use an inclusive range instead
17+
1418
pattern_analysis_uncovered = {$count ->
1519
[1] pattern `{$witness_1}`
1620
[2] patterns `{$witness_1}` and `{$witness_2}`

compiler/rustc_pattern_analysis/src/constructor.rs

+20-16
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ impl fmt::Display for RangeEnd {
193193
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
194194
pub enum MaybeInfiniteInt {
195195
NegInfinity,
196-
/// Encoded value. DO NOT CONSTRUCT BY HAND; use `new_finite`.
196+
/// Encoded value. DO NOT CONSTRUCT BY HAND; use `new_finite_{int,uint}`.
197197
#[non_exhaustive]
198198
Finite(u128),
199199
/// The integer after `u128::MAX`. We need it to represent `x..=u128::MAX` as an exclusive range.
@@ -230,25 +230,22 @@ impl MaybeInfiniteInt {
230230
}
231231

232232
/// Note: this will not turn a finite value into an infinite one or vice-versa.
233-
pub fn minus_one(self) -> Self {
233+
pub fn minus_one(self) -> Option<Self> {
234234
match self {
235-
Finite(n) => match n.checked_sub(1) {
236-
Some(m) => Finite(m),
237-
None => panic!("Called `MaybeInfiniteInt::minus_one` on 0"),
238-
},
239-
JustAfterMax => Finite(u128::MAX),
240-
x => x,
235+
Finite(n) => n.checked_sub(1).map(Finite),
236+
JustAfterMax => Some(Finite(u128::MAX)),
237+
x => Some(x),
241238
}
242239
}
243240
/// Note: this will not turn a finite value into an infinite one or vice-versa.
244-
pub fn plus_one(self) -> Self {
241+
pub fn plus_one(self) -> Option<Self> {
245242
match self {
246243
Finite(n) => match n.checked_add(1) {
247-
Some(m) => Finite(m),
248-
None => JustAfterMax,
244+
Some(m) => Some(Finite(m)),
245+
None => Some(JustAfterMax),
249246
},
250-
JustAfterMax => panic!("Called `MaybeInfiniteInt::plus_one` on u128::MAX+1"),
251-
x => x,
247+
JustAfterMax => None,
248+
x => Some(x),
252249
}
253250
}
254251
}
@@ -269,18 +266,25 @@ impl IntRange {
269266
pub fn is_singleton(&self) -> bool {
270267
// Since `lo` and `hi` can't be the same `Infinity` and `plus_one` never changes from finite
271268
// to infinite, this correctly only detects ranges that contain exacly one `Finite(x)`.
272-
self.lo.plus_one() == self.hi
269+
// `unwrap()` is ok since `self.lo` will never be `JustAfterMax`.
270+
self.lo.plus_one().unwrap() == self.hi
273271
}
274272

273+
/// Construct a singleton range.
274+
/// `x` be a `Finite(_)` value.
275275
#[inline]
276276
pub fn from_singleton(x: MaybeInfiniteInt) -> IntRange {
277-
IntRange { lo: x, hi: x.plus_one() }
277+
// `unwrap()` is ok on a finite value
278+
IntRange { lo: x, hi: x.plus_one().unwrap() }
278279
}
279280

281+
/// Construct a range with these boundaries.
282+
/// `lo` must not be `PosInfinity` or `JustAfterMax`. `hi` must not be `NegInfinity`.
283+
/// If `end` is `Included`, `hi` must also not be `JustAfterMax`.
280284
#[inline]
281285
pub fn from_range(lo: MaybeInfiniteInt, mut hi: MaybeInfiniteInt, end: RangeEnd) -> IntRange {
282286
if end == RangeEnd::Included {
283-
hi = hi.plus_one();
287+
hi = hi.plus_one().unwrap();
284288
}
285289
if lo >= hi {
286290
// This should have been caught earlier by E0030.

compiler/rustc_pattern_analysis/src/errors.rs

+37
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,43 @@ impl<'tcx> AddToDiagnostic for Overlap<'tcx> {
7272
}
7373
}
7474

75+
#[derive(LintDiagnostic)]
76+
#[diag(pattern_analysis_small_gap_between_ranges)]
77+
pub struct SmallGapBetweenRanges<'tcx> {
78+
#[label]
79+
#[suggestion(code = "{suggestion}", applicability = "maybe-incorrect")]
80+
/// This is an exclusive range that looks like `lo..gap` (i.e. doesn't match `gap`).
81+
pub first_range: Span,
82+
/// Suggest `lo..=gap` instead.
83+
pub suggestion: String,
84+
pub gap: Pat<'tcx>,
85+
#[subdiagnostic]
86+
/// All these ranges skipped over `gap` which we think is probably a mistake.
87+
pub gap_with: Vec<GappedRange<'tcx>>,
88+
}
89+
90+
pub struct GappedRange<'tcx> {
91+
pub span: Span,
92+
pub gap: Pat<'tcx>,
93+
pub first_range: Pat<'tcx>,
94+
}
95+
96+
impl<'tcx> AddToDiagnostic for GappedRange<'tcx> {
97+
fn add_to_diagnostic_with<F>(self, diag: &mut Diagnostic, _: F)
98+
where
99+
F: Fn(&mut Diagnostic, SubdiagnosticMessage) -> SubdiagnosticMessage,
100+
{
101+
let GappedRange { span, gap, first_range } = self;
102+
103+
// FIXME(mejrs) unfortunately `#[derive(LintDiagnostic)]`
104+
// does not support `#[subdiagnostic(eager)]`...
105+
let message = format!(
106+
"this seems to continue range `{first_range}`, but `{gap}` isn't matched by either of them"
107+
);
108+
diag.span_label(span, message);
109+
}
110+
}
111+
75112
#[derive(LintDiagnostic)]
76113
#[diag(pattern_analysis_non_exhaustive_omitted_pattern)]
77114
#[help]

compiler/rustc_pattern_analysis/src/lib.rs

+16-18
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,7 @@ use rustc_index::Idx;
2626
use rustc_middle::ty::Ty;
2727

2828
use crate::constructor::{Constructor, ConstructorSet};
29-
#[cfg(feature = "rustc")]
30-
use crate::lints::{
31-
lint_nonexhaustive_missing_variants, lint_overlapping_range_endpoints, PatternColumn,
32-
};
3329
use crate::pat::DeconstructedPat;
34-
#[cfg(feature = "rustc")]
35-
use crate::rustc::RustcMatchCheckCtxt;
36-
#[cfg(feature = "rustc")]
37-
use crate::usefulness::{compute_match_usefulness, ValidityConstraint};
3830

3931
// It's not possible to only enable the `typed_arena` dependency when the `rustc` feature is off, so
4032
// we use another feature instead. The crate won't compile if one of these isn't enabled.
@@ -107,27 +99,33 @@ pub struct MatchArm<'p, Cx: TypeCx> {
10799
/// The entrypoint for this crate. Computes whether a match is exhaustive and which of its arms are
108100
/// useful, and runs some lints.
109101
#[cfg(feature = "rustc")]
110-
pub fn analyze_match<'p, 'thir, 'tcx>(
111-
tycx: &RustcMatchCheckCtxt<'p, 'thir, 'tcx>,
102+
pub fn analyze_match<'p, 'thir, 'tcx: 'thir>(
103+
tycx: &rustc::RustcMatchCheckCtxt<'p, 'thir, 'tcx>,
112104
arms: &[rustc::MatchArm<'p, 'thir, 'tcx>],
113105
scrut_ty: Ty<'tcx>,
114106
) -> rustc::UsefulnessReport<'p, 'thir, 'tcx> {
107+
use crate::lints::PatternColumn;
108+
use crate::usefulness::ValidityConstraint;
109+
115110
// Arena to store the extra wildcards we construct during analysis.
116111
let wildcard_arena = tycx.pattern_arena;
117112
let scrut_validity = ValidityConstraint::from_bool(tycx.known_valid_scrutinee);
118113
let cx = MatchCtxt { tycx, wildcard_arena };
119114

120-
let report = compute_match_usefulness(cx, arms, scrut_ty, scrut_validity);
115+
let report = usefulness::compute_match_usefulness(cx, arms, scrut_ty, scrut_validity);
121116

122-
let pat_column = PatternColumn::new(arms);
117+
// Only run the lints if the match is exhaustive.
118+
if report.non_exhaustiveness_witnesses.is_empty() {
119+
let pat_column = PatternColumn::new(arms);
123120

124-
// Lint on ranges that overlap on their endpoints, which is likely a mistake.
125-
lint_overlapping_range_endpoints(cx, &pat_column);
121+
// Detect possible range-related mistakes.
122+
lints::lint_likely_range_mistakes(cx, &pat_column);
126123

127-
// Run the non_exhaustive_omitted_patterns lint. Only run on refutable patterns to avoid hitting
128-
// `if let`s. Only run if the match is exhaustive otherwise the error is redundant.
129-
if tycx.refutable && report.non_exhaustiveness_witnesses.is_empty() {
130-
lint_nonexhaustive_missing_variants(cx, arms, &pat_column, scrut_ty)
124+
// Run the non_exhaustive_omitted_patterns lint. Only run on refutable patterns to avoid
125+
// hitting `if let`s.
126+
if tycx.refutable {
127+
lints::lint_nonexhaustive_missing_variants(cx, arms, &pat_column, scrut_ty)
128+
}
131129
}
132130

133131
report

0 commit comments

Comments
 (0)