2023-06-16 21:39:02 +02:00
|
|
|
use clippy_utils::diagnostics::span_lint_and_sugg;
|
|
|
|
use rustc_ast::LitKind;
|
|
|
|
use rustc_data_structures::fx::FxHashSet;
|
|
|
|
use rustc_errors::Applicability;
|
2023-07-03 17:42:48 +00:00
|
|
|
use rustc_hir::{Expr, ExprKind, PatKind, RangeEnd, UnOp};
|
|
|
|
use rustc_lint::{LateContext, LateLintPass, LintContext};
|
2023-06-16 21:39:02 +02:00
|
|
|
use rustc_middle::lint::in_external_macro;
|
|
|
|
use rustc_session::{declare_lint_pass, declare_tool_lint};
|
|
|
|
|
|
|
|
declare_clippy_lint! {
|
|
|
|
/// ### What it does
|
|
|
|
/// Looks for combined OR patterns that are all contained in a specific range,
|
|
|
|
/// e.g. `6 | 4 | 5 | 9 | 7 | 8` can be rewritten as `4..=9`.
|
|
|
|
///
|
|
|
|
/// ### Why is this bad?
|
|
|
|
/// Using an explicit range is more concise and easier to read.
|
|
|
|
///
|
2023-07-06 17:05:26 +02:00
|
|
|
/// ### Known issues
|
|
|
|
/// This lint intentionally does not handle numbers greater than `i128::MAX` for `u128` literals
|
|
|
|
/// in order to support negative numbers.
|
|
|
|
///
|
2023-06-16 21:39:02 +02:00
|
|
|
/// ### Example
|
|
|
|
/// ```rust
|
|
|
|
/// let x = 6;
|
|
|
|
/// let foo = matches!(x, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10);
|
|
|
|
/// ```
|
|
|
|
/// Use instead:
|
|
|
|
/// ```rust
|
|
|
|
/// let x = 6;
|
|
|
|
/// let foo = matches!(x, 1..=10);
|
|
|
|
/// ```
|
|
|
|
#[clippy::version = "1.72.0"]
|
2023-06-20 10:51:39 +02:00
|
|
|
pub MANUAL_RANGE_PATTERNS,
|
2023-06-16 21:39:02 +02:00
|
|
|
complexity,
|
|
|
|
"manually writing range patterns using a combined OR pattern (`|`)"
|
|
|
|
}
|
2023-06-20 10:51:39 +02:00
|
|
|
declare_lint_pass!(ManualRangePatterns => [MANUAL_RANGE_PATTERNS]);
|
2023-06-16 21:39:02 +02:00
|
|
|
|
2023-07-03 20:24:14 +02:00
|
|
|
fn expr_as_i128(expr: &Expr<'_>) -> Option<i128> {
|
|
|
|
if let ExprKind::Unary(UnOp::Neg, expr) = expr.kind {
|
|
|
|
expr_as_i128(expr).map(|num| -num)
|
|
|
|
} else if let ExprKind::Lit(lit) = expr.kind
|
2023-06-16 21:39:02 +02:00
|
|
|
&& let LitKind::Int(num, _) = lit.node
|
|
|
|
{
|
2023-07-06 17:05:26 +02:00
|
|
|
// Intentionally not handling numbers greater than i128::MAX (for u128 literals) for now.
|
|
|
|
num.try_into().ok()
|
2023-06-16 21:39:02 +02:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-20 10:51:39 +02:00
|
|
|
impl LateLintPass<'_> for ManualRangePatterns {
|
2023-06-16 21:39:02 +02:00
|
|
|
fn check_pat(&mut self, cx: &LateContext<'_>, pat: &'_ rustc_hir::Pat<'_>) {
|
|
|
|
if in_external_macro(cx.sess(), pat.span) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// a pattern like 1 | 2 seems fine, lint if there are at least 3 alternatives
|
|
|
|
if let PatKind::Or(pats) = pat.kind
|
|
|
|
&& pats.len() >= 3
|
|
|
|
{
|
2023-07-03 20:24:14 +02:00
|
|
|
let mut min = i128::MAX;
|
|
|
|
let mut max = i128::MIN;
|
2023-06-16 21:39:02 +02:00
|
|
|
let mut numbers_found = FxHashSet::default();
|
|
|
|
let mut ranges_found = Vec::new();
|
|
|
|
|
|
|
|
for pat in pats {
|
|
|
|
if let PatKind::Lit(lit) = pat.kind
|
2023-07-03 20:24:14 +02:00
|
|
|
&& let Some(num) = expr_as_i128(lit)
|
2023-06-16 21:39:02 +02:00
|
|
|
{
|
|
|
|
numbers_found.insert(num);
|
|
|
|
|
|
|
|
min = min.min(num);
|
|
|
|
max = max.max(num);
|
|
|
|
} else if let PatKind::Range(Some(left), Some(right), end) = pat.kind
|
2023-07-03 20:24:14 +02:00
|
|
|
&& let Some(left) = expr_as_i128(left)
|
|
|
|
&& let Some(right) = expr_as_i128(right)
|
2023-06-16 21:39:02 +02:00
|
|
|
&& right >= left
|
|
|
|
{
|
|
|
|
min = min.min(left);
|
|
|
|
max = max.max(right);
|
|
|
|
ranges_found.push(left..=match end {
|
|
|
|
RangeEnd::Included => right,
|
|
|
|
RangeEnd::Excluded => right - 1,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let contains_whole_range = 'contains: {
|
|
|
|
let mut num = min;
|
|
|
|
while num <= max {
|
|
|
|
if numbers_found.contains(&num) {
|
|
|
|
num += 1;
|
|
|
|
}
|
|
|
|
// Given a list of (potentially overlapping) ranges like:
|
|
|
|
// 1..=5, 3..=7, 6..=10
|
|
|
|
// We want to find the range with the highest end that still contains the current number
|
|
|
|
else if let Some(range) = ranges_found
|
|
|
|
.iter()
|
|
|
|
.filter(|range| range.contains(&num))
|
|
|
|
.max_by_key(|range| range.end())
|
|
|
|
{
|
|
|
|
num = range.end() + 1;
|
|
|
|
} else {
|
|
|
|
break 'contains false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break 'contains true;
|
|
|
|
};
|
|
|
|
|
|
|
|
if contains_whole_range {
|
|
|
|
span_lint_and_sugg(
|
|
|
|
cx,
|
2023-06-20 10:51:39 +02:00
|
|
|
MANUAL_RANGE_PATTERNS,
|
2023-06-16 21:39:02 +02:00
|
|
|
pat.span,
|
|
|
|
"this OR pattern can be rewritten using a range",
|
|
|
|
"try",
|
|
|
|
format!("{min}..={max}"),
|
|
|
|
Applicability::MachineApplicable,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|