https://gcc.gnu.org/g:548fbf6f2c9c2acb1e6fdde93d217ee042eafd23
commit r17-2243-g548fbf6f2c9c2acb1e6fdde93d217ee042eafd23 Author: Lucas Ly Ba <[email protected]> Date: Sun Jun 28 20:30:27 2026 +0200 gccrs: add non_contiguous_range_endpoints lint Warn when an exclusive range pattern and the next arm are one value apart, such as `20..30` followed by `31..=40`, which silently skips 30. gcc/rust/ChangeLog: * checks/lints/unused/rust-unused-checker.cc (UnusedChecker::visit): New. * checks/lints/unused/rust-unused-checker.h (UnusedChecker::visit): New. gcc/testsuite/ChangeLog: * rust/compile/non-contiguous-range-endpoints_0.rs: New test. Signed-off-by: Lucas Ly Ba <[email protected]> Diff: --- .../checks/lints/unused/rust-unused-checker.cc | 108 +++++++++++++++++++++ gcc/rust/checks/lints/unused/rust-unused-checker.h | 1 + .../compile/non-contiguous-range-endpoints_0.rs | 20 ++++ 3 files changed, 129 insertions(+) diff --git a/gcc/rust/checks/lints/unused/rust-unused-checker.cc b/gcc/rust/checks/lints/unused/rust-unused-checker.cc index 7d875daef339..30fb37cfb99b 100644 --- a/gcc/rust/checks/lints/unused/rust-unused-checker.cc +++ b/gcc/rust/checks/lints/unused/rust-unused-checker.cc @@ -20,6 +20,7 @@ #include "rust-hir-expr.h" #include "rust-hir-generic-param.h" #include "rust-hir-item.h" +#include "rust-hir-pattern.h" #include "options.h" #include "rust-keyword-values.h" @@ -215,5 +216,112 @@ UnusedChecker::visit (HIR::StructPatternFieldIdentPat &field) walk (field); } +namespace { + +bool +literal_int_value (const HIR::Literal &lit, bool minus, int64_t &out) +{ + if (lit.get_lit_type () != HIR::Literal::LitType::INT) + return false; + + std::string digits = lit.as_string (); + digits.erase (std::remove (digits.begin (), digits.end (), '_'), + digits.end ()); + + char *end = nullptr; + long long value = std::strtoll (digits.c_str (), &end, 10); + if (end == digits.c_str () || *end != '\0') + return false; + + out = minus ? -value : value; + return true; +} + +bool +range_bound_int (HIR::RangePatternBound &bound, int64_t &out) +{ + if (bound.get_bound_type () + != HIR::RangePatternBound::RangePatternBoundType::LITERAL) + return false; + + auto &lit = static_cast<HIR::RangePatternBoundLiteral &> (bound); + return literal_int_value (lit.get_literal (), lit.get_has_minus (), out); +} + +} // namespace + +void +UnusedChecker::visit (HIR::MatchExpr &expr) +{ + struct Range + { + int64_t lo; + int64_t hi; + bool inclusive; + location_t locus; + }; + std::vector<Range> ranges; + std::vector<int64_t> starts; + + for (auto &match_case : expr.get_match_cases ()) + { + auto &pattern = match_case.get_arm ().get_pattern (); + if (!pattern) + continue; + + if (pattern->get_pattern_type () == HIR::Pattern::PatternType::RANGE) + { + auto &range = static_cast<HIR::RangePattern &> (*pattern); + int64_t lo, hi; + if (range_bound_int (range.get_lower_bound (), lo) + && range_bound_int (range.get_upper_bound (), hi)) + { + ranges.push_back ( + {lo, hi, range.is_inclusive_range (), range.get_locus ()}); + starts.push_back (lo); + } + } + else if (pattern->get_pattern_type () + == HIR::Pattern::PatternType::LITERAL) + { + auto &lit = static_cast<HIR::LiteralPattern &> (*pattern); + int64_t value; + if (literal_int_value (lit.get_literal (), lit.get_has_minus (), + value)) + starts.push_back (value); + } + } + + // A value is covered if a range matches it or some arm starts exactly on it. + auto covered = [&] (int64_t value) { + if (std::find (starts.begin (), starts.end (), value) != starts.end ()) + return true; + for (auto &range : ranges) + { + int64_t top = range.inclusive ? range.hi : range.hi - 1; + if (value >= range.lo && value <= top) + return true; + } + return false; + }; + + // An exclusive range `lo..hi` leaves `hi` unmatched. If another arm picks up + // at `hi + 1`, that single value was almost certainly meant to be included. + for (auto &range : ranges) + { + if (range.inclusive) + continue; + + int64_t missed = range.hi; + if (!covered (missed) + && std::find (starts.begin (), starts.end (), missed + 1) + != starts.end ()) + rust_warning_at (range.locus, OPT_Wunused_variable, + "multiple ranges are one apart"); + } + + walk (expr); +} + } // namespace Analysis } // namespace Rust diff --git a/gcc/rust/checks/lints/unused/rust-unused-checker.h b/gcc/rust/checks/lints/unused/rust-unused-checker.h index 055d49b193e1..c8a272041a9d 100644 --- a/gcc/rust/checks/lints/unused/rust-unused-checker.h +++ b/gcc/rust/checks/lints/unused/rust-unused-checker.h @@ -49,6 +49,7 @@ private: virtual void visit (HIR::Module &mod) override; virtual void visit (HIR::LifetimeParam &lft) override; virtual void visit (HIR::StructPatternFieldIdentPat &field) override; + virtual void visit (HIR::MatchExpr &expr) override; virtual void visit_loop_label (HIR::LoopLabel &label) override; }; } // namespace Analysis diff --git a/gcc/testsuite/rust/compile/non-contiguous-range-endpoints_0.rs b/gcc/testsuite/rust/compile/non-contiguous-range-endpoints_0.rs new file mode 100644 index 000000000000..4afff8a25582 --- /dev/null +++ b/gcc/testsuite/rust/compile/non-contiguous-range-endpoints_0.rs @@ -0,0 +1,20 @@ +// { dg-additional-options "-frust-unused-check-2.0" } +#![feature(no_core, exclusive_range_pattern)] +#![no_core] + +pub fn f(x: u8) -> u8 { + match x { + 20..30 => 1, +// { dg-warning "multiple ranges are one apart" "" { target *-*-* } .-1 } + 31..=40 => 2, + _ => 0, + } +} + +pub fn g(x: u8) -> u8 { + match x { + 0..10 => 1, + 10..=20 => 2, + _ => 0, + } +}
