Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f538ce7

Browse files
committedDec 19, 2023
Lint small gaps between ranges
1 parent 57ad505 commit f538ce7

File tree

12 files changed

+397
-82
lines changed

12 files changed

+397
-82
lines changed
 

‎compiler/rustc_lint_defs/src/builtin.rs

+30
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,35 @@ 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+
/// let x = 123u32;
849+
/// match x {
850+
/// 0..100 => { println!("small"); }
851+
/// 101..1000 => { println!("large"); }
852+
/// _ => { println!("larger"); }
853+
/// }
854+
/// ```
855+
///
856+
/// {{produces}}
857+
///
858+
/// ### Explanation
859+
///
860+
/// It is likely a mistake to have range patterns in a match expression that miss out a single
861+
/// number. Check that the beginning and end values are what you expect, and keep in mind that
862+
/// with `..=` the right bound is inclusive, and with `..` it is exclusive.
863+
pub SMALL_GAPS_BETWEEN_RANGES,
864+
Warn,
865+
"detects range patterns separated by a single number"
866+
}
867+
838868
declare_lint! {
839869
/// The `bindings_with_variant_name` lint detects pattern bindings with
840870
/// 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 = ... 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

+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

+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

+15-17
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.
@@ -109,26 +101,32 @@ impl<'p, Cx: TypeCx> Copy for MatchArm<'p, Cx> {}
109101
/// useful, and runs some lints.
110102
#[cfg(feature = "rustc")]
111103
pub fn analyze_match<'p, 'tcx>(
112-
tycx: &RustcMatchCheckCtxt<'p, 'tcx>,
104+
tycx: &rustc::RustcMatchCheckCtxt<'p, 'tcx>,
113105
arms: &[rustc::MatchArm<'p, 'tcx>],
114106
scrut_ty: Ty<'tcx>,
115107
) -> rustc::UsefulnessReport<'p, 'tcx> {
108+
use crate::lints::PatternColumn;
109+
use crate::usefulness::ValidityConstraint;
110+
116111
// Arena to store the extra wildcards we construct during analysis.
117112
let wildcard_arena = tycx.pattern_arena;
118113
let scrut_validity = ValidityConstraint::from_bool(tycx.known_valid_scrutinee);
119114
let cx = MatchCtxt { tycx, wildcard_arena };
120115

121-
let report = compute_match_usefulness(cx, arms, scrut_ty, scrut_validity);
116+
let report = usefulness::compute_match_usefulness(cx, arms, scrut_ty, scrut_validity);
122117

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

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

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

134132
report

‎compiler/rustc_pattern_analysis/src/lints.rs

+61-20
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ use rustc_session::lint::builtin::NON_EXHAUSTIVE_OMITTED_PATTERNS;
77
use rustc_span::Span;
88

99
use crate::constructor::{IntRange, MaybeInfiniteInt};
10-
use crate::errors::{
11-
NonExhaustiveOmittedPattern, NonExhaustiveOmittedPatternLintOnArm, Overlap,
12-
OverlappingRangeEndpoints, Uncovered,
13-
};
10+
use crate::errors;
1411
use crate::rustc::{
1512
Constructor, DeconstructedPat, MatchArm, MatchCtxt, PlaceCtxt, RustcMatchCheckCtxt,
1613
SplitConstructorSet, WitnessPat,
@@ -189,9 +186,9 @@ pub(crate) fn lint_nonexhaustive_missing_variants<'a, 'p, 'tcx>(
189186
NON_EXHAUSTIVE_OMITTED_PATTERNS,
190187
rcx.match_lint_level,
191188
rcx.scrut_span,
192-
NonExhaustiveOmittedPattern {
189+
errors::NonExhaustiveOmittedPattern {
193190
scrut_ty,
194-
uncovered: Uncovered::new(rcx.scrut_span, rcx, witnesses),
191+
uncovered: errors::Uncovered::new(rcx.scrut_span, rcx, witnesses),
195192
},
196193
);
197194
}
@@ -203,7 +200,7 @@ pub(crate) fn lint_nonexhaustive_missing_variants<'a, 'p, 'tcx>(
203200
let (lint_level, lint_level_source) =
204201
rcx.tcx.lint_level_at_node(NON_EXHAUSTIVE_OMITTED_PATTERNS, arm.arm_data);
205202
if !matches!(lint_level, rustc_session::lint::Level::Allow) {
206-
let decorator = NonExhaustiveOmittedPatternLintOnArm {
203+
let decorator = errors::NonExhaustiveOmittedPatternLintOnArm {
207204
lint_span: lint_level_source.span(),
208205
suggest_lint_on_match: rcx.whole_match_span.map(|span| span.shrink_to_lo()),
209206
lint_level: lint_level.as_str(),
@@ -220,9 +217,10 @@ pub(crate) fn lint_nonexhaustive_missing_variants<'a, 'p, 'tcx>(
220217
}
221218
}
222219

223-
/// Traverse the patterns to warn the user about ranges that overlap on their endpoints.
220+
/// Traverse the patterns to warn the user about ranges that overlap on their endpoints or are
221+
/// distant by one.
224222
#[instrument(level = "debug", skip(cx))]
225-
pub(crate) fn lint_overlapping_range_endpoints<'a, 'p, 'tcx>(
223+
pub(crate) fn lint_likely_range_mistakes<'a, 'p, 'tcx>(
226224
cx: MatchCtxt<'a, 'p, 'tcx>,
227225
column: &PatternColumn<'a, 'p, 'tcx>,
228226
) {
@@ -235,24 +233,24 @@ pub(crate) fn lint_overlapping_range_endpoints<'a, 'p, 'tcx>(
235233
let set = column.analyze_ctors(pcx);
236234

237235
if matches!(ty.kind(), ty::Char | ty::Int(_) | ty::Uint(_)) {
238-
let emit_lint = |overlap: &IntRange, this_span: Span, overlapped_spans: &[Span]| {
236+
let emit_overlap_lint = |overlap: &IntRange, this_span: Span, overlapped_spans: &[Span]| {
239237
let overlap_as_pat = rcx.hoist_pat_range(overlap, ty);
240238
let overlaps: Vec<_> = overlapped_spans
241239
.iter()
242240
.copied()
243-
.map(|span| Overlap { range: overlap_as_pat.clone(), span })
241+
.map(|span| errors::Overlap { range: overlap_as_pat.clone(), span })
244242
.collect();
245243
rcx.tcx.emit_spanned_lint(
246244
lint::builtin::OVERLAPPING_RANGE_ENDPOINTS,
247245
rcx.match_lint_level,
248246
this_span,
249-
OverlappingRangeEndpoints { overlap: overlaps, range: this_span },
247+
errors::OverlappingRangeEndpoints { overlap: overlaps, range: this_span },
250248
);
251249
};
252250

253-
// If two ranges overlapped, the split set will contain their intersection as a singleton.
254-
let split_int_ranges = set.present.iter().filter_map(|c| c.as_int_range());
255-
for overlap_range in split_int_ranges.clone() {
251+
// The two cases we are interested in will show up as a singleton after range splitting.
252+
let present_int_ranges = set.present.iter().filter_map(|c| c.as_int_range());
253+
for overlap_range in present_int_ranges {
256254
if overlap_range.is_singleton() {
257255
let overlap: MaybeInfiniteInt = overlap_range.lo;
258256
// Ranges that look like `lo..=overlap`.
@@ -267,29 +265,72 @@ pub(crate) fn lint_overlapping_range_endpoints<'a, 'p, 'tcx>(
267265
// Don't lint when one of the ranges is a singleton.
268266
continue;
269267
}
270-
if this_range.lo == overlap {
268+
if overlap == this_range.lo {
271269
// `this_range` looks like `overlap..=this_range.hi`; it overlaps with any
272270
// ranges that look like `lo..=overlap`.
273271
if !prefixes.is_empty() {
274-
emit_lint(overlap_range, this_span, &prefixes);
272+
emit_overlap_lint(overlap_range, this_span, &prefixes);
275273
}
276274
suffixes.push(this_span)
277-
} else if this_range.hi == overlap.plus_one() {
275+
} else if overlap.plus_one() == Some(this_range.hi) {
278276
// `this_range` looks like `this_range.lo..=overlap`; it overlaps with any
279277
// ranges that look like `overlap..=hi`.
280278
if !suffixes.is_empty() {
281-
emit_lint(overlap_range, this_span, &suffixes);
279+
emit_overlap_lint(overlap_range, this_span, &suffixes);
282280
}
283281
prefixes.push(this_span)
284282
}
285283
}
286284
}
287285
}
286+
287+
let missing_int_ranges = set.missing.iter().filter_map(|c| c.as_int_range());
288+
for point_range in missing_int_ranges {
289+
if point_range.is_singleton() {
290+
let point: MaybeInfiniteInt = point_range.lo;
291+
// Ranges that look like `lo..point`.
292+
let mut onebefore: SmallVec<[_; 1]> = Default::default();
293+
// Ranges that look like `point+1..=hi`.
294+
let mut oneafter: SmallVec<[_; 1]> = Default::default();
295+
for pat in column.iter() {
296+
let this_span = *pat.data();
297+
let Constructor::IntRange(this_range) = pat.ctor() else { continue };
298+
299+
if point == this_range.hi && !this_range.is_singleton() {
300+
onebefore.push(this_span)
301+
} else if point.plus_one() == Some(this_range.lo) {
302+
oneafter.push(this_span)
303+
}
304+
}
305+
306+
if !onebefore.is_empty() && !oneafter.is_empty() {
307+
// We have some `lo..point` and some `point+1..hi` but no `point`.
308+
let point_as_pat = rcx.hoist_pat_range(point_range, ty);
309+
for span_after in oneafter {
310+
let spans_before: Vec<_> = onebefore
311+
.iter()
312+
.copied()
313+
.map(|span| errors::GappedRange { range: point_as_pat.clone(), span })
314+
.collect();
315+
rcx.tcx.emit_spanned_lint(
316+
lint::builtin::SMALL_GAPS_BETWEEN_RANGES,
317+
rcx.match_lint_level,
318+
span_after,
319+
errors::SmallGapBetweenRanges {
320+
range: point_as_pat.clone(),
321+
first_range: span_after,
322+
gap_with: spans_before,
323+
},
324+
);
325+
}
326+
}
327+
}
328+
}
288329
} else {
289330
// Recurse into the fields.
290331
for ctor in set.present {
291332
for col in column.specialize(pcx, &ctor) {
292-
lint_overlapping_range_endpoints(cx, &col);
333+
lint_likely_range_mistakes(cx, &col);
293334
}
294335
}
295336
}

‎compiler/rustc_pattern_analysis/src/rustc.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -659,12 +659,12 @@ impl<'p, 'tcx> RustcMatchCheckCtxt<'p, 'tcx> {
659659
let value = mir::Const::from_ty_const(c, cx.tcx);
660660
lo = PatRangeBoundary::Finite(value);
661661
}
662-
let hi = if matches!(range.hi, Finite(0)) {
662+
let hi = if let Some(hi) = range.hi.minus_one() {
663+
hi
664+
} else {
663665
// The range encodes `..ty::MIN`, so we can't convert it to an inclusive range.
664666
end = rustc_hir::RangeEnd::Excluded;
665667
range.hi
666-
} else {
667-
range.hi.minus_one()
668668
};
669669
let hi = cx.hoist_pat_range_bdy(hi, ty);
670670
PatKind::Range(Box::new(PatRange { lo, hi, end, ty }))
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Failed to load comments.