Skip to content

Commit 8b07d6f

Browse files
committed
Auto merge of #118879 - Nadrieril:lint-range-gap, r=<try>
WIP: Lint small gaps between ranges In the discussion to stabilize exclusive range patterns (#37854), it has often come up that they're likely to cause off-by-one mistakes. We already have the `overlapping_range_endpoint` lint, so I [proposed](#37854 (comment)) the `small_gap_between_ranges` lint as a proof of concept. Here's the idea (see the test file to get a better idea): ```rust match x { 0..10 => ..., // WARN: this range has a small gap on `10_u8`... 11..20 => ..., // ... with this range. _ => ..., } // note: you likely meant to match `10_u8` too ``` For now the PR is meant as illustration so the discussion can progress. I'll make this a real lint if we end up wanting it (and if we can find a good name). r? ghost
2 parents 27d8a57 + 89053d1 commit 8b07d6f

File tree

8 files changed

+338
-48
lines changed

8 files changed

+338
-48
lines changed

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 = ... with this range
16+
.note = you likely meant to match `{$range}` too
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

+22-16
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,12 @@ enum Presence {
180180
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
181181
pub enum MaybeInfiniteInt {
182182
NegInfinity,
183-
/// Encoded value. DO NOT CONSTRUCT BY HAND; use `new_finite`.
183+
/// Encoded value. DO NOT CONSTRUCT BY HAND; use `new_finite_{int,uint}`.
184184
#[non_exhaustive]
185185
Finite(u128),
186186
/// The integer after `u128::MAX`. We need it to represent `x..=u128::MAX` as an exclusive range.
187+
// Users likely shouldn't be constructing it directly.
188+
#[non_exhaustive]
187189
JustAfterMax,
188190
PosInfinity,
189191
}
@@ -217,25 +219,22 @@ impl MaybeInfiniteInt {
217219
}
218220

219221
/// Note: this will not turn a finite value into an infinite one or vice-versa.
220-
pub fn minus_one(self) -> Self {
222+
pub fn minus_one(self) -> Option<Self> {
221223
match self {
222-
Finite(n) => match n.checked_sub(1) {
223-
Some(m) => Finite(m),
224-
None => bug!(),
225-
},
226-
JustAfterMax => Finite(u128::MAX),
227-
x => x,
224+
Finite(n) => n.checked_sub(1).map(Finite),
225+
JustAfterMax => Some(Finite(u128::MAX)),
226+
x => Some(x),
228227
}
229228
}
230229
/// Note: this will not turn a finite value into an infinite one or vice-versa.
231-
pub fn plus_one(self) -> Self {
230+
pub fn plus_one(self) -> Option<Self> {
232231
match self {
233232
Finite(n) => match n.checked_add(1) {
234-
Some(m) => Finite(m),
235-
None => JustAfterMax,
233+
Some(m) => Some(Finite(m)),
234+
None => Some(JustAfterMax),
236235
},
237-
JustAfterMax => bug!(),
238-
x => x,
236+
JustAfterMax => None,
237+
x => Some(x),
239238
}
240239
}
241240
}
@@ -256,18 +255,25 @@ impl IntRange {
256255
pub(crate) fn is_singleton(&self) -> bool {
257256
// Since `lo` and `hi` can't be the same `Infinity` and `plus_one` never changes from finite
258257
// to infinite, this correctly only detects ranges that contain exacly one `Finite(x)`.
259-
self.lo.plus_one() == self.hi
258+
// `unwrap()` is ok since `self.lo` will never be `JustAfterMax`.
259+
self.lo.plus_one().unwrap() == self.hi
260260
}
261261

262+
/// Construct a singleton range.
263+
/// `x` be a `Finite(_)` value.
262264
#[inline]
263265
pub fn from_singleton(x: MaybeInfiniteInt) -> IntRange {
264-
IntRange { lo: x, hi: x.plus_one() }
266+
// `unwrap()` is ok on a finite value
267+
IntRange { lo: x, hi: x.plus_one().unwrap() }
265268
}
266269

270+
/// Construct a range with these boundaries.
271+
/// `lo` must not be `PosInfinity` or `JustAfterMax`. `hi` must not be `NegInfinity`.
272+
/// If `end` is `Included`, `hi` must also not be `JustAfterMax`.
267273
#[inline]
268274
pub fn from_range(lo: MaybeInfiniteInt, mut hi: MaybeInfiniteInt, end: RangeEnd) -> IntRange {
269275
if end == RangeEnd::Included {
270-
hi = hi.plus_one();
276+
hi = hi.plus_one().unwrap();
271277
}
272278
if lo >= hi {
273279
// This should have been caught earlier by E0030.

compiler/rustc_pattern_analysis/src/cx.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -635,12 +635,12 @@ impl<'p, 'tcx> MatchCheckCtxt<'p, 'tcx> {
635635
let value = mir::Const::from_ty_const(c, cx.tcx);
636636
lo = PatRangeBoundary::Finite(value);
637637
}
638-
let hi = if matches!(range.hi, Finite(0)) {
638+
let hi = if let Some(hi) = range.hi.minus_one() {
639+
hi
640+
} else {
639641
// The range encodes `..ty::MIN`, so we can't convert it to an inclusive range.
640642
end = RangeEnd::Excluded;
641643
range.hi
642-
} else {
643-
range.hi.minus_one()
644644
};
645645
let hi = cx.hoist_pat_range_bdy(hi, ty);
646646
PatKind::Range(Box::new(PatRange { lo, hi, end, ty }))

compiler/rustc_pattern_analysis/src/errors.rs

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

75+
#[derive(LintDiagnostic)]
76+
#[diag(pattern_analysis_small_gap_between_ranges)]
77+
#[note]
78+
pub struct SmallGapBetweenRanges<'tcx> {
79+
#[label]
80+
pub first_range: Span,
81+
pub range: Pat<'tcx>,
82+
#[subdiagnostic]
83+
pub gap_with: Vec<GappedRange<'tcx>>,
84+
}
85+
86+
pub struct GappedRange<'tcx> {
87+
pub span: Span,
88+
pub range: Pat<'tcx>,
89+
}
90+
91+
impl<'tcx> AddToDiagnostic for GappedRange<'tcx> {
92+
fn add_to_diagnostic_with<F>(self, diag: &mut Diagnostic, _: F)
93+
where
94+
F: Fn(&mut Diagnostic, SubdiagnosticMessage) -> SubdiagnosticMessage,
95+
{
96+
let GappedRange { span, range } = self;
97+
98+
// FIXME(mejrs) unfortunately `#[derive(LintDiagnostic)]`
99+
// does not support `#[subdiagnostic(eager)]`...
100+
let message = format!("this range has a small gap on `{range}`...");
101+
diag.span_label(span, message);
102+
}
103+
}
104+
75105
#[derive(LintDiagnostic)]
76106
#[diag(pattern_analysis_non_exhaustive_omitted_pattern)]
77107
#[help]

compiler/rustc_pattern_analysis/src/lib.rs

+11-9
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ rustc_fluent_macro::fluent_messages! { "../messages.ftl" }
1717
use lints::PatternColumn;
1818
use rustc_hir::HirId;
1919
use rustc_middle::ty::Ty;
20-
use usefulness::{compute_match_usefulness, UsefulnessReport};
20+
use usefulness::UsefulnessReport;
2121

2222
use crate::cx::MatchCheckCtxt;
23-
use crate::lints::{lint_nonexhaustive_missing_variants, lint_overlapping_range_endpoints};
2423
use crate::pat::DeconstructedPat;
2524

2625
/// The arm of a match expression.
@@ -41,15 +40,18 @@ pub fn analyze_match<'p, 'tcx>(
4140
) -> UsefulnessReport<'p, 'tcx> {
4241
let pat_column = PatternColumn::new(arms);
4342

44-
let report = compute_match_usefulness(cx, arms, scrut_ty);
43+
let report = usefulness::compute_match_usefulness(cx, arms, scrut_ty);
4544

46-
// Lint on ranges that overlap on their endpoints, which is likely a mistake.
47-
lint_overlapping_range_endpoints(cx, &pat_column);
45+
// Only run the lints if the match is exhaustive.
46+
if report.non_exhaustiveness_witnesses.is_empty() {
47+
// Detect possible range-related mistakes.
48+
lints::lint_likely_range_mistakes(cx, &pat_column);
4849

49-
// Run the non_exhaustive_omitted_patterns lint. Only run on refutable patterns to avoid hitting
50-
// `if let`s. Only run if the match is exhaustive otherwise the error is redundant.
51-
if cx.refutable && report.non_exhaustiveness_witnesses.is_empty() {
52-
lint_nonexhaustive_missing_variants(cx, arms, &pat_column, scrut_ty)
50+
// Run the non_exhaustive_omitted_patterns lint. Only run on refutable patterns to avoid
51+
// hitting `if let`s.
52+
if cx.refutable {
53+
lints::lint_nonexhaustive_missing_variants(cx, arms, &pat_column, scrut_ty)
54+
}
5355
}
5456

5557
report

compiler/rustc_pattern_analysis/src/lints.rs

+61-20
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ use rustc_span::Span;
88

99
use crate::constructor::{Constructor, IntRange, MaybeInfiniteInt, SplitConstructorSet};
1010
use crate::cx::MatchCheckCtxt;
11-
use crate::errors::{
12-
NonExhaustiveOmittedPattern, NonExhaustiveOmittedPatternLintOnArm, Overlap,
13-
OverlappingRangeEndpoints, Uncovered,
14-
};
11+
use crate::errors;
1512
use crate::pat::{DeconstructedPat, WitnessPat};
1613
use crate::usefulness::PatCtxt;
1714
use crate::MatchArm;
@@ -184,9 +181,9 @@ pub(crate) fn lint_nonexhaustive_missing_variants<'p, 'tcx>(
184181
NON_EXHAUSTIVE_OMITTED_PATTERNS,
185182
cx.match_lint_level,
186183
cx.scrut_span,
187-
NonExhaustiveOmittedPattern {
184+
errors::NonExhaustiveOmittedPattern {
188185
scrut_ty,
189-
uncovered: Uncovered::new(cx.scrut_span, cx, witnesses),
186+
uncovered: errors::Uncovered::new(cx.scrut_span, cx, witnesses),
190187
},
191188
);
192189
}
@@ -198,7 +195,7 @@ pub(crate) fn lint_nonexhaustive_missing_variants<'p, 'tcx>(
198195
let (lint_level, lint_level_source) =
199196
cx.tcx.lint_level_at_node(NON_EXHAUSTIVE_OMITTED_PATTERNS, arm.hir_id);
200197
if !matches!(lint_level, rustc_session::lint::Level::Allow) {
201-
let decorator = NonExhaustiveOmittedPatternLintOnArm {
198+
let decorator = errors::NonExhaustiveOmittedPatternLintOnArm {
202199
lint_span: lint_level_source.span(),
203200
suggest_lint_on_match: cx.whole_match_span.map(|span| span.shrink_to_lo()),
204201
lint_level: lint_level.as_str(),
@@ -215,9 +212,10 @@ pub(crate) fn lint_nonexhaustive_missing_variants<'p, 'tcx>(
215212
}
216213
}
217214

218-
/// Traverse the patterns to warn the user about ranges that overlap on their endpoints.
215+
/// Traverse the patterns to warn the user about ranges that overlap on their endpoints or are
216+
/// distant by one.
219217
#[instrument(level = "debug", skip(cx))]
220-
pub(crate) fn lint_overlapping_range_endpoints<'p, 'tcx>(
218+
pub(crate) fn lint_likely_range_mistakes<'p, 'tcx>(
221219
cx: &MatchCheckCtxt<'p, 'tcx>,
222220
column: &PatternColumn<'p, 'tcx>,
223221
) {
@@ -229,24 +227,24 @@ pub(crate) fn lint_overlapping_range_endpoints<'p, 'tcx>(
229227
let set = column.analyze_ctors(pcx);
230228

231229
if matches!(ty.kind(), ty::Char | ty::Int(_) | ty::Uint(_)) {
232-
let emit_lint = |overlap: &IntRange, this_span: Span, overlapped_spans: &[Span]| {
230+
let emit_overlap_lint = |overlap: &IntRange, this_span: Span, overlapped_spans: &[Span]| {
233231
let overlap_as_pat = cx.hoist_pat_range(overlap, ty);
234232
let overlaps: Vec<_> = overlapped_spans
235233
.iter()
236234
.copied()
237-
.map(|span| Overlap { range: overlap_as_pat.clone(), span })
235+
.map(|span| errors::Overlap { range: overlap_as_pat.clone(), span })
238236
.collect();
239237
cx.tcx.emit_spanned_lint(
240238
lint::builtin::OVERLAPPING_RANGE_ENDPOINTS,
241239
cx.match_lint_level,
242240
this_span,
243-
OverlappingRangeEndpoints { overlap: overlaps, range: this_span },
241+
errors::OverlappingRangeEndpoints { overlap: overlaps, range: this_span },
244242
);
245243
};
246244

247-
// If two ranges overlapped, the split set will contain their intersection as a singleton.
248-
let split_int_ranges = set.present.iter().filter_map(|c| c.as_int_range());
249-
for overlap_range in split_int_ranges.clone() {
245+
// The two cases we are interested in will show up as a singleton after range splitting.
246+
let present_int_ranges = set.present.iter().filter_map(|c| c.as_int_range());
247+
for overlap_range in present_int_ranges {
250248
if overlap_range.is_singleton() {
251249
let overlap: MaybeInfiniteInt = overlap_range.lo;
252250
// Ranges that look like `lo..=overlap`.
@@ -261,29 +259,72 @@ pub(crate) fn lint_overlapping_range_endpoints<'p, 'tcx>(
261259
// Don't lint when one of the ranges is a singleton.
262260
continue;
263261
}
264-
if this_range.lo == overlap {
262+
if overlap == this_range.lo {
265263
// `this_range` looks like `overlap..=this_range.hi`; it overlaps with any
266264
// ranges that look like `lo..=overlap`.
267265
if !prefixes.is_empty() {
268-
emit_lint(overlap_range, this_span, &prefixes);
266+
emit_overlap_lint(overlap_range, this_span, &prefixes);
269267
}
270268
suffixes.push(this_span)
271-
} else if this_range.hi == overlap.plus_one() {
269+
} else if overlap.plus_one() == Some(this_range.hi) {
272270
// `this_range` looks like `this_range.lo..=overlap`; it overlaps with any
273271
// ranges that look like `overlap..=hi`.
274272
if !suffixes.is_empty() {
275-
emit_lint(overlap_range, this_span, &suffixes);
273+
emit_overlap_lint(overlap_range, this_span, &suffixes);
276274
}
277275
prefixes.push(this_span)
278276
}
279277
}
280278
}
281279
}
280+
281+
let missing_int_ranges = set.missing.iter().filter_map(|c| c.as_int_range());
282+
for point_range in missing_int_ranges {
283+
if point_range.is_singleton() {
284+
let point: MaybeInfiniteInt = point_range.lo;
285+
// Ranges that look like `lo..point`.
286+
let mut onebefore: SmallVec<[_; 1]> = Default::default();
287+
// Ranges that look like `point+1..=hi`.
288+
let mut oneafter: SmallVec<[_; 1]> = Default::default();
289+
for pat in column.iter() {
290+
let this_span = pat.span();
291+
let Constructor::IntRange(this_range) = pat.ctor() else { continue };
292+
293+
if point == this_range.hi {
294+
onebefore.push(this_span)
295+
} else if point.plus_one() == Some(this_range.lo) {
296+
oneafter.push(this_span)
297+
}
298+
}
299+
300+
if !onebefore.is_empty() && !oneafter.is_empty() {
301+
// We have some `lo..point` and some `point+1..hi` but no `point`.
302+
let point_as_pat = cx.hoist_pat_range(point_range, ty);
303+
for span_after in oneafter {
304+
let spans_before: Vec<_> = onebefore
305+
.iter()
306+
.copied()
307+
.map(|span| errors::GappedRange { range: point_as_pat.clone(), span })
308+
.collect();
309+
cx.tcx.emit_spanned_lint(
310+
lint::builtin::OVERLAPPING_RANGE_ENDPOINTS,
311+
cx.match_lint_level,
312+
span_after,
313+
errors::SmallGapBetweenRanges {
314+
range: point_as_pat.clone(),
315+
first_range: span_after,
316+
gap_with: spans_before,
317+
},
318+
);
319+
}
320+
}
321+
}
322+
}
282323
} else {
283324
// Recurse into the fields.
284325
for ctor in set.present {
285326
for col in column.specialize(pcx, &ctor) {
286-
lint_overlapping_range_endpoints(cx, &col);
327+
lint_likely_range_mistakes(cx, &col);
287328
}
288329
}
289330
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#![feature(exclusive_range_pattern)]
2+
#![deny(overlapping_range_endpoints)]
3+
4+
macro_rules! m {
5+
($s:expr, $t1:pat, $t2:pat) => {
6+
match $s {
7+
$t1 => {}
8+
$t2 => {}
9+
_ => {}
10+
}
11+
};
12+
}
13+
14+
fn main() {
15+
m!(0u8, 20..30, 31..=40); //~ ERROR multiple ranges are one apart
16+
m!(0u8, 31..=40, 20..30); //~ ERROR multiple ranges are one apart
17+
m!(0u8, 20..30, 29..=40); //~ ERROR multiple patterns overlap on their endpoints
18+
m!(0u8, 20..30, 30..=40);
19+
m!(0u8, 20..30, 31..=40); //~ ERROR multiple ranges are one apart
20+
m!(0u8, 20..30, 32..=40);
21+
m!(0u8, 20..30, 31); //~ ERROR multiple ranges are one apart
22+
m!(0u8, 20..30, 31..=32); //~ ERROR multiple ranges are one apart
23+
m!(0u8, 30, 30..=40);
24+
m!(0u8, 30, 31..=40);
25+
m!(0u8, 30, 32..=40); //~ ERROR multiple ranges are one apart
26+
27+
match 0u8 {
28+
0..10 => {}
29+
10 => {}
30+
11..20 => {}
31+
_ => {}
32+
}
33+
34+
match 0u8 {
35+
0..10 => {}
36+
21..30 => {} //~ ERROR multiple ranges are one apart
37+
11..20 => {} //~ ERROR multiple ranges are one apart
38+
_ => {}
39+
}
40+
match (0u8, true) {
41+
(0..10, true) => {}
42+
(11..20, true) => {} //~ ERROR multiple ranges are one apart
43+
(11..20, false) => {} //~ ERROR multiple ranges are one apart
44+
_ => {}
45+
}
46+
match (true, 0u8) {
47+
(true, 0..10) => {}
48+
(true, 11..20) => {} //~ ERROR multiple ranges are one apart
49+
(false, 11..20) => {} //~ ERROR multiple ranges are one apart
50+
_ => {}
51+
}
52+
match Some(0u8) {
53+
Some(0..10) => {}
54+
Some(11..20) => {} //~ ERROR multiple ranges are one apart
55+
_ => {}
56+
}
57+
}

0 commit comments

Comments
 (0)