Skip to content

Commit

Permalink
Auto merge of rust-lang#116734 - Nadrieril:lint-per-column, r=<try>
Browse files Browse the repository at this point in the history
Lint `non_exhaustive_omitted_patterns` by columns

This is a rework of the `non_exhaustive_omitted_patterns` lint to make it more consistent. The intent of the lint is to help consumers of `non_exhaustive` enums ensure they stay up-to-date with all upstream variants. This rewrite fixes two cases we didn't handle well before:

First, because of details of exhaustiveness checking, the following wouldn't lint `Enum::C` as missing:
```rust
match Some(x) {
    Some(Enum::A) => {}
    Some(Enum::B) => {}
    _ => {}
}
```

Second, because of the fundamental workings of exhaustiveness checking, the following would treat the `true` and `false` cases separately and thus lint about missing variants:
```rust
match (true, x) {
    (true, Enum::A) => {}
    (true, Enum::B) => {}
    (false, Enum::C) => {}
    _ => {}
}
```
Moreover, it would correctly not lint in the case where the pair is flipped, because of asymmetry in how exhaustiveness checking proceeds.

A drawback is that it no longer makes sense to set the lint level per-arm. This will silently break the lint for current users of it (but it's behind a feature gate so that's ok).

The new approach is now independent of the exhaustiveness algorithm; it's a separate pass that looks at patterns column by column. This is another of the motivations for this: I'm glad to move it out of the algorithm, it was akward there.

This PR is almost identical to rust-lang#111651. cc `@eholk` who reviewed it at the time. Compared to then, I'm more confident this is the right approach.
  • Loading branch information
bors committed Oct 14, 2023
2 parents 495c5dd + 493111e commit 2d1ddb5
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 369 deletions.
34 changes: 21 additions & 13 deletions compiler/rustc_lint_defs/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3953,8 +3953,13 @@ declare_lint! {
}

declare_lint! {
/// The `non_exhaustive_omitted_patterns` lint detects when a wildcard (`_` or `..`) in a
/// pattern for a `#[non_exhaustive]` struct or enum is reachable.
/// The `non_exhaustive_omitted_patterns` lint aims to help consumers of a `#[non_exhaustive]`
/// struct or enum who want to match all of its fields/variants explicitly.
///
/// The `#[non_exhaustive]` annotation forces matches to use wildcards, so exhaustiveness
/// checking cannot be used to ensure that all fields/variants are matched explicitly. To remedy
/// this, this allow-by-default lint warns the user when a match mentions some but not all of
/// the fields/variants of a `#[non_exhaustive]` struct or enum.
///
/// ### Example
///
Expand All @@ -3968,39 +3973,42 @@ declare_lint! {
///
/// // in crate B
/// #![feature(non_exhaustive_omitted_patterns_lint)]
/// #[warn(non_exhaustive_omitted_patterns)]
/// match Bar::A {
/// Bar::A => {},
/// #[warn(non_exhaustive_omitted_patterns)]
/// _ => {},
/// }
/// ```
///
/// This will produce:
///
/// ```text
/// warning: reachable patterns not covered of non exhaustive enum
/// warning: some variants are not matched explicitly
/// --> $DIR/reachable-patterns.rs:70:9
/// |
/// LL | _ => {}
/// | ^ pattern `B` not covered
/// LL | match Bar::A {
/// | ^ pattern `Bar::B` not covered
/// |
/// note: the lint level is defined here
/// --> $DIR/reachable-patterns.rs:69:16
/// |
/// LL | #[warn(non_exhaustive_omitted_patterns)]
/// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/// = help: ensure that all possible cases are being handled by adding the suggested match arms
/// = help: ensure that all variants are matched explicitly by adding the suggested match arms
/// = note: the matched value is of type `Bar` and the `non_exhaustive_omitted_patterns` attribute was found
/// ```
///
/// Warning: setting this to `deny` will make upstream non-breaking changes (adding fields or
/// variants to a `#[non_exhaustive]` struct or enum) break your crate. This goes against
/// expected semver behavior.
///
/// ### Explanation
///
/// Structs and enums tagged with `#[non_exhaustive]` force the user to add a
/// (potentially redundant) wildcard when pattern-matching, to allow for future
/// addition of fields or variants. The `non_exhaustive_omitted_patterns` lint
/// detects when such a wildcard happens to actually catch some fields/variants.
/// In other words, when the match without the wildcard would not be exhaustive.
/// This lets the user be informed if new fields/variants were added.
/// Structs and enums tagged with `#[non_exhaustive]` force the user to add a (potentially
/// redundant) wildcard when pattern-matching, to allow for future addition of fields or
/// variants. The `non_exhaustive_omitted_patterns` lint detects when such a wildcard happens to
/// actually catch some fields/variants. In other words, when the match without the wildcard
/// would not be exhaustive. This lets the user be informed if new fields/variants were added.
pub NON_EXHAUSTIVE_OMITTED_PATTERNS,
Allow,
"detect when patterns of types marked `non_exhaustive` are missed",
Expand Down
4 changes: 2 additions & 2 deletions compiler/rustc_mir_build/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
fluent_generated as fluent,
thir::pattern::{deconstruct_pat::DeconstructedPat, MatchCheckCtxt},
thir::pattern::{deconstruct_pat::WitnessPat, MatchCheckCtxt},
};
use rustc_errors::{
error_code, AddToDiagnostic, Applicability, Diagnostic, DiagnosticBuilder, ErrorGuaranteed,
Expand Down Expand Up @@ -810,7 +810,7 @@ impl<'tcx> Uncovered<'tcx> {
pub fn new<'p>(
span: Span,
cx: &MatchCheckCtxt<'p, 'tcx>,
witnesses: Vec<DeconstructedPat<'p, 'tcx>>,
witnesses: Vec<WitnessPat<'tcx>>,
) -> Self {
let witness_1 = witnesses.get(0).unwrap().to_pat(cx);
Self {
Expand Down
24 changes: 12 additions & 12 deletions compiler/rustc_mir_build/src/thir/pattern/check_match.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::deconstruct_pat::{Constructor, DeconstructedPat};
use super::deconstruct_pat::{Constructor, DeconstructedPat, WitnessPat};
use super::usefulness::{
compute_match_usefulness, MatchArm, MatchCheckCtxt, Reachability, UsefulnessReport,
};
Expand Down Expand Up @@ -269,7 +269,7 @@ impl<'p, 'tcx> MatchVisitor<'_, 'p, 'tcx> {

let scrut = &self.thir[scrut];
let scrut_ty = scrut.ty;
let report = compute_match_usefulness(&cx, &tarms, self.lint_level, scrut_ty);
let report = compute_match_usefulness(&cx, &tarms, self.lint_level, scrut_ty, scrut.span);

match source {
// Don't report arm reachability of desugared `match $iter.into_iter() { iter => .. }`
Expand Down Expand Up @@ -431,7 +431,7 @@ impl<'p, 'tcx> MatchVisitor<'_, 'p, 'tcx> {
let pattern = self.lower_pattern(&mut cx, pat);
let pattern_ty = pattern.ty();
let arm = MatchArm { pat: pattern, hir_id: self.lint_level, has_guard: false };
let report = compute_match_usefulness(&cx, &[arm], self.lint_level, pattern_ty);
let report = compute_match_usefulness(&cx, &[arm], self.lint_level, pattern_ty, pattern.span());

// Note: we ignore whether the pattern is unreachable (i.e. whether the type is empty). We
// only care about exhaustiveness here.
Expand Down Expand Up @@ -622,7 +622,7 @@ fn is_let_irrefutable<'p, 'tcx>(
pat: &'p DeconstructedPat<'p, 'tcx>,
) -> bool {
let arms = [MatchArm { pat, hir_id: pat_id, has_guard: false }];
let report = compute_match_usefulness(&cx, &arms, pat_id, pat.ty());
let report = compute_match_usefulness(&cx, &arms, pat_id, pat.ty(), pat.span());

// Report if the pattern is unreachable, which can only occur when the type is uninhabited.
// This also reports unreachable sub-patterns though, so we can't just replace it with an
Expand Down Expand Up @@ -661,8 +661,8 @@ fn report_arm_reachability<'p, 'tcx>(
}
}

fn collect_non_exhaustive_tys<'p, 'tcx>(
pat: &DeconstructedPat<'p, 'tcx>,
fn collect_non_exhaustive_tys<'tcx>(
pat: &WitnessPat<'tcx>,
non_exhaustive_tys: &mut FxHashSet<Ty<'tcx>>,
) {
if matches!(pat.ctor(), Constructor::NonExhaustive) {
Expand All @@ -678,7 +678,7 @@ fn non_exhaustive_match<'p, 'tcx>(
thir: &Thir<'tcx>,
scrut_ty: Ty<'tcx>,
sp: Span,
witnesses: Vec<DeconstructedPat<'p, 'tcx>>,
witnesses: Vec<WitnessPat<'tcx>>,
arms: &[ArmId],
expr_span: Span,
) -> ErrorGuaranteed {
Expand Down Expand Up @@ -860,10 +860,10 @@ fn non_exhaustive_match<'p, 'tcx>(

pub(crate) fn joined_uncovered_patterns<'p, 'tcx>(
cx: &MatchCheckCtxt<'p, 'tcx>,
witnesses: &[DeconstructedPat<'p, 'tcx>],
witnesses: &[WitnessPat<'tcx>],
) -> String {
const LIMIT: usize = 3;
let pat_to_str = |pat: &DeconstructedPat<'p, 'tcx>| pat.to_pat(cx).to_string();
let pat_to_str = |pat: &WitnessPat<'tcx>| pat.to_pat(cx).to_string();
match witnesses {
[] => bug!(),
[witness] => format!("`{}`", witness.to_pat(cx)),
Expand All @@ -880,7 +880,7 @@ pub(crate) fn joined_uncovered_patterns<'p, 'tcx>(
}

pub(crate) fn pattern_not_covered_label(
witnesses: &[DeconstructedPat<'_, '_>],
witnesses: &[WitnessPat<'_>],
joined_patterns: &str,
) -> String {
format!("pattern{} {} not covered", rustc_errors::pluralize!(witnesses.len()), joined_patterns)
Expand All @@ -891,7 +891,7 @@ fn adt_defined_here<'p, 'tcx>(
cx: &MatchCheckCtxt<'p, 'tcx>,
err: &mut Diagnostic,
ty: Ty<'tcx>,
witnesses: &[DeconstructedPat<'p, 'tcx>],
witnesses: &[WitnessPat<'tcx>],
) {
let ty = ty.peel_refs();
if let ty::Adt(def, _) = ty.kind() {
Expand Down Expand Up @@ -922,7 +922,7 @@ fn adt_defined_here<'p, 'tcx>(
fn maybe_point_at_variant<'a, 'p: 'a, 'tcx: 'a>(
cx: &MatchCheckCtxt<'p, 'tcx>,
def: AdtDef<'tcx>,
patterns: impl Iterator<Item = &'a DeconstructedPat<'p, 'tcx>>,
patterns: impl Iterator<Item = &'a WitnessPat<'tcx>>,
) -> Vec<Span> {
use Constructor::*;
let mut covered = vec![];
Expand Down
Loading

0 comments on commit 2d1ddb5

Please sign in to comment.