[gcc r17-2243] gccrs: add non_contiguous_range_endpoints lint

Arthur Cohen via Gcc-cvs <[email protected]>
Newsgroups gmane.comp.gcc.cvs
Message-ID <[email protected]>
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,
+    }
+}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.