Skip to content

Commit

Permalink
Lint small gaps between ranges
Browse files Browse the repository at this point in the history
  • Loading branch information
Nadrieril committed Mar 9, 2024
1 parent f783043 commit 8ac9a04
Show file tree
Hide file tree
Showing 7 changed files with 492 additions and 12 deletions.
8 changes: 8 additions & 0 deletions compiler/rustc_pattern_analysis/messages.ftl
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
pattern_analysis_excluside_range_missing_gap = multiple ranges are one apart
.label = this range doesn't match `{$gap}` because `..` is an exclusive range
.suggestion = use an inclusive range instead
pattern_analysis_excluside_range_missing_max = exclusive range missing `{$max}`
.label = this range doesn't match `{$max}` because `..` is an exclusive range
.suggestion = use an inclusive range instead
pattern_analysis_non_exhaustive_omitted_pattern = some variants are not matched explicitly
.help = ensure that all variants are matched explicitly by adding the suggested match arms
.note = the matched value is of type `{$scrut_ty}` and the `non_exhaustive_omitted_patterns` attribute was found
Expand Down
51 changes: 51 additions & 0 deletions compiler/rustc_pattern_analysis/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,57 @@ impl<'tcx> AddToDiagnostic for Overlap<'tcx> {
}
}

#[derive(LintDiagnostic)]
#[diag(pattern_analysis_excluside_range_missing_max)]
pub struct ExclusiveRangeMissingMax<'tcx> {
#[label]
#[suggestion(code = "{suggestion}", applicability = "maybe-incorrect")]
/// This is an exclusive range that looks like `lo..max` (i.e. doesn't match `max`).
pub first_range: Span,
/// Suggest `lo..=max` instead.
pub suggestion: String,
pub max: Pat<'tcx>,
}

#[derive(LintDiagnostic)]
#[diag(pattern_analysis_excluside_range_missing_gap)]
pub struct ExclusiveRangeMissingGap<'tcx> {
#[label]
#[suggestion(code = "{suggestion}", applicability = "maybe-incorrect")]
/// This is an exclusive range that looks like `lo..gap` (i.e. doesn't match `gap`).
pub first_range: Span,
pub gap: Pat<'tcx>,
/// Suggest `lo..=gap` instead.
pub suggestion: String,
#[subdiagnostic]
/// All these ranges skipped over `gap` which we think is probably a mistake.
pub gap_with: Vec<GappedRange<'tcx>>,
}

pub struct GappedRange<'tcx> {
pub span: Span,
pub gap: Pat<'tcx>,
pub first_range: Pat<'tcx>,
}

impl<'tcx> AddToDiagnostic for GappedRange<'tcx> {
fn add_to_diagnostic_with<G: EmissionGuarantee, F: SubdiagMessageOp<G>>(
self,
diag: &mut Diag<'_, G>,
_: F,
) {
let GappedRange { span, gap, first_range } = self;

// FIXME(mejrs) unfortunately `#[derive(LintDiagnostic)]`
// does not support `#[subdiagnostic(eager)]`...
let message = format!(
"this could appear to continue range `{first_range}`, but `{gap}` isn't matched by \
either of them"
);
diag.span_label(span, message);
}
}

#[derive(LintDiagnostic)]
#[diag(pattern_analysis_non_exhaustive_omitted_pattern)]
#[help]
Expand Down
23 changes: 16 additions & 7 deletions compiler/rustc_pattern_analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,8 @@ use rustc_middle::ty::Ty;
use rustc_span::ErrorGuaranteed;

use crate::constructor::{Constructor, ConstructorSet, IntRange};
#[cfg(feature = "rustc")]
use crate::lints::lint_nonexhaustive_missing_variants;
use crate::pat::DeconstructedPat;
use crate::pat_column::PatternColumn;
#[cfg(feature = "rustc")]
use crate::rustc::RustcMatchCheckCtxt;
#[cfg(feature = "rustc")]
use crate::usefulness::{compute_match_usefulness, ValidityConstraint};

pub trait Captures<'a> {}
impl<'a, T: ?Sized> Captures<'a> for T {}
Expand Down Expand Up @@ -145,6 +139,18 @@ pub trait TypeCx: Sized + fmt::Debug {

/// The maximum pattern complexity limit was reached.
fn complexity_exceeded(&self) -> Result<(), Self::Error>;

/// Lint that there is a gap `gap` between `pat` and all of `gapped_with` such that the gap is
/// not matched by another range. If `gapped_with` is empty, then `gap` is `T::MAX`. We only
/// detect singleton gaps.
/// The default implementation does nothing.
fn lint_non_contiguous_range_endpoints(
&self,
_pat: &DeconstructedPat<Self>,
_gap: IntRange,
_gapped_with: &[&DeconstructedPat<Self>],
) {
}
}

/// The arm of a match expression.
Expand All @@ -167,11 +173,14 @@ impl<'p, Cx: TypeCx> Copy for MatchArm<'p, Cx> {}
/// useful, and runs some lints.
#[cfg(feature = "rustc")]
pub fn analyze_match<'p, 'tcx>(
tycx: &RustcMatchCheckCtxt<'p, 'tcx>,
tycx: &rustc::RustcMatchCheckCtxt<'p, 'tcx>,
arms: &[rustc::MatchArm<'p, 'tcx>],
scrut_ty: Ty<'tcx>,
pattern_complexity_limit: Option<usize>,
) -> Result<rustc::UsefulnessReport<'p, 'tcx>, ErrorGuaranteed> {
use lints::lint_nonexhaustive_missing_variants;
use usefulness::{compute_match_usefulness, ValidityConstraint};

let scrut_ty = tycx.reveal_opaque_ty(scrut_ty);
let scrut_validity = ValidityConstraint::from_bool(tycx.known_valid_scrutinee);
let report =
Expand Down
66 changes: 65 additions & 1 deletion compiler/rustc_pattern_analysis/src/rustc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use rustc_index::{Idx, IndexVec};
use rustc_middle::middle::stability::EvalResult;
use rustc_middle::mir::interpret::Scalar;
use rustc_middle::mir::{self, Const};
use rustc_middle::thir::{FieldPat, Pat, PatKind, PatRange, PatRangeBoundary};
use rustc_middle::thir::{self, FieldPat, Pat, PatKind, PatRange, PatRangeBoundary};
use rustc_middle::ty::layout::IntegerExt;
use rustc_middle::ty::{self, FieldDef, OpaqueTypeKey, Ty, TyCtxt, TypeVisitableExt, VariantDef};
use rustc_session::lint;
Expand Down Expand Up @@ -900,6 +900,70 @@ impl<'p, 'tcx: 'p> TypeCx for RustcMatchCheckCtxt<'p, 'tcx> {
let span = self.whole_match_span.unwrap_or(self.scrut_span);
Err(self.tcx.dcx().span_err(span, "reached pattern complexity limit"))
}

fn lint_non_contiguous_range_endpoints(
&self,
pat: &crate::pat::DeconstructedPat<Self>,
gap: IntRange,
gapped_with: &[&crate::pat::DeconstructedPat<Self>],
) {
let Some(&thir_pat) = pat.data() else { return };
let thir::PatKind::Range(range) = &thir_pat.kind else { return };
// Only lint when the left range is an exclusive range.
if range.end != rustc_hir::RangeEnd::Excluded {
return;
}
// `pat` is an exclusive range like `lo..gap`. `gapped_with` contains ranges that start with
// `gap+1`.
let suggested_range: thir::Pat<'_> = {
// Suggest `lo..=gap` instead.
let mut suggested_range = thir_pat.clone();
let thir::PatKind::Range(range) = &mut suggested_range.kind else { unreachable!() };
range.end = rustc_hir::RangeEnd::Included;
suggested_range
};
let gap_as_pat = self.hoist_pat_range(&gap, *pat.ty());
if gapped_with.is_empty() {
// If `gapped_with` is empty, `gap == T::MAX`.
self.tcx.emit_node_span_lint(
lint::builtin::NON_CONTIGUOUS_RANGE_ENDPOINTS,
self.match_lint_level,
thir_pat.span,
errors::ExclusiveRangeMissingMax {
// Point at this range.
first_range: thir_pat.span,
// That's the gap that isn't covered.
max: gap_as_pat.clone(),
// Suggest `lo..=max` instead.
suggestion: suggested_range.to_string(),
},
);
} else {
self.tcx.emit_node_span_lint(
lint::builtin::NON_CONTIGUOUS_RANGE_ENDPOINTS,
self.match_lint_level,
thir_pat.span,
errors::ExclusiveRangeMissingGap {
// Point at this range.
first_range: thir_pat.span,
// That's the gap that isn't covered.
gap: gap_as_pat.clone(),
// Suggest `lo..=gap` instead.
suggestion: suggested_range.to_string(),
// All these ranges skipped over `gap` which we think is probably a
// mistake.
gap_with: gapped_with
.iter()
.map(|pat| errors::GappedRange {
span: pat.data().unwrap().span,
gap: gap_as_pat.clone(),
first_range: thir_pat.clone(),
})
.collect(),
},
);
}
}
}

/// Recursively expand this pattern into its subpatterns. Only useful for or-patterns.
Expand Down
46 changes: 42 additions & 4 deletions compiler/rustc_pattern_analysis/src/usefulness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1489,7 +1489,7 @@ impl<Cx: TypeCx> WitnessMatrix<Cx> {
/// We can however get false negatives because exhaustiveness does not explore all cases. See the
/// section on relevancy at the top of the file.
fn collect_overlapping_range_endpoints<'p, Cx: TypeCx>(
mcx: &mut UsefulnessCtxt<'_, Cx>,
cx: &Cx,
overlap_range: IntRange,
matrix: &Matrix<'p, Cx>,
specialized_matrix: &Matrix<'p, Cx>,
Expand Down Expand Up @@ -1522,7 +1522,7 @@ fn collect_overlapping_range_endpoints<'p, Cx: TypeCx>(
.map(|&(_, pat)| pat)
.collect();
if !overlaps_with.is_empty() {
mcx.tycx.lint_overlapping_range_endpoints(pat, overlap_range, &overlaps_with);
cx.lint_overlapping_range_endpoints(pat, overlap_range, &overlaps_with);
}
}
suffixes.push((child_row_id, pat))
Expand All @@ -1538,14 +1538,41 @@ fn collect_overlapping_range_endpoints<'p, Cx: TypeCx>(
.map(|&(_, pat)| pat)
.collect();
if !overlaps_with.is_empty() {
mcx.tycx.lint_overlapping_range_endpoints(pat, overlap_range, &overlaps_with);
cx.lint_overlapping_range_endpoints(pat, overlap_range, &overlaps_with);
}
}
prefixes.push((child_row_id, pat))
}
}
}

/// Collect ranges that have a singleton gap between them.
fn collect_non_contiguous_range_endpoints<'p, Cx: TypeCx>(
cx: &Cx,
gap_range: &IntRange,
matrix: &Matrix<'p, Cx>,
) {
let gap = gap_range.lo;
// Ranges that look like `lo..gap`.
let mut onebefore: SmallVec<[_; 1]> = Default::default();
// Ranges that start on `gap+1` or singletons `gap+1`.
let mut oneafter: SmallVec<[_; 1]> = Default::default();
// Look through the column for ranges near the gap.
for pat in matrix.heads() {
let PatOrWild::Pat(pat) = pat else { continue };
let Constructor::IntRange(this_range) = pat.ctor() else { continue };
if gap == this_range.hi {
onebefore.push(pat)
} else if gap.plus_one() == Some(this_range.lo) {
oneafter.push(pat)
}
}

for pat_before in onebefore {
cx.lint_non_contiguous_range_endpoints(pat_before, *gap_range, oneafter.as_slice());
}
}

/// The core of the algorithm.
///
/// This recursively computes witnesses of the non-exhaustiveness of `matrix` (if any). Also tracks
Expand Down Expand Up @@ -1626,13 +1653,24 @@ fn compute_exhaustiveness_and_usefulness<'a, 'p, Cx: TypeCx>(
&& spec_matrix.rows.len() >= 2
&& spec_matrix.rows.iter().any(|row| !row.intersects.is_empty())
{
collect_overlapping_range_endpoints(mcx, overlap_range, matrix, &spec_matrix);
collect_overlapping_range_endpoints(mcx.tycx, overlap_range, matrix, &spec_matrix);
}
}

matrix.unspecialize(spec_matrix);
}

// Detect singleton gaps between ranges.
if missing_ctors.iter().any(|c| matches!(c, Constructor::IntRange(..))) {
for missing in &missing_ctors {
if let Constructor::IntRange(gap) = missing {
if gap.is_singleton() {
collect_non_contiguous_range_endpoints(mcx.tycx, gap, matrix);
}
}
}
}

// Record usefulness in the patterns.
for row in matrix.rows() {
if row.useful {
Expand Down

0 comments on commit 8ac9a04

Please sign in to comment.