Skip to content

Commit 2466a05

Browse files
committed
Auto merge of #8918 - Jarcho:almost_complete_letter_range, r=llogiq
Add lint `almost_complete_letter_range` fixes #7269 changelog: Add lint `almost_complete_letter_range`
2 parents e1607e9 + eb2908b commit 2466a05

11 files changed

+361
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3276,6 +3276,7 @@ Released 2018-09-13
32763276
<!-- begin autogenerated links to lint list -->
32773277
[`absurd_extreme_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#absurd_extreme_comparisons
32783278
[`allow_attributes_without_reason`]: https://rust-lang.github.io/rust-clippy/master/index.html#allow_attributes_without_reason
3279+
[`almost_complete_letter_range`]: https://rust-lang.github.io/rust-clippy/master/index.html#almost_complete_letter_range
32793280
[`almost_swapped`]: https://rust-lang.github.io/rust-clippy/master/index.html#almost_swapped
32803281
[`approx_constant`]: https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
32813282
[`as_conversions`]: https://rust-lang.github.io/rust-clippy/master/index.html#as_conversions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::source::{trim_span, walk_span_to_context};
3+
use clippy_utils::{meets_msrv, msrvs};
4+
use rustc_ast::ast::{Expr, ExprKind, LitKind, Pat, PatKind, RangeEnd, RangeLimits};
5+
use rustc_errors::Applicability;
6+
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
7+
use rustc_semver::RustcVersion;
8+
use rustc_session::{declare_tool_lint, impl_lint_pass};
9+
use rustc_span::Span;
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
/// Checks for ranges which almost include the entire range of letters from 'a' to 'z', but
14+
/// don't because they're a half open range.
15+
///
16+
/// ### Why is this bad?
17+
/// This (`'a'..'z'`) is almost certainly a typo meant to include all letters.
18+
///
19+
/// ### Example
20+
/// ```rust
21+
/// let _ = 'a'..'z';
22+
/// ```
23+
/// Use instead:
24+
/// ```rust
25+
/// let _ = 'a'..='z';
26+
/// ```
27+
#[clippy::version = "1.63.0"]
28+
pub ALMOST_COMPLETE_LETTER_RANGE,
29+
suspicious,
30+
"almost complete letter range"
31+
}
32+
impl_lint_pass!(AlmostCompleteLetterRange => [ALMOST_COMPLETE_LETTER_RANGE]);
33+
34+
pub struct AlmostCompleteLetterRange {
35+
msrv: Option<RustcVersion>,
36+
}
37+
impl AlmostCompleteLetterRange {
38+
pub fn new(msrv: Option<RustcVersion>) -> Self {
39+
Self { msrv }
40+
}
41+
}
42+
impl EarlyLintPass for AlmostCompleteLetterRange {
43+
fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &Expr) {
44+
if let ExprKind::Range(Some(start), Some(end), RangeLimits::HalfOpen) = &e.kind {
45+
let ctxt = e.span.ctxt();
46+
let sugg = if let Some(start) = walk_span_to_context(start.span, ctxt)
47+
&& let Some(end) = walk_span_to_context(end.span, ctxt)
48+
&& meets_msrv(self.msrv, msrvs::RANGE_INCLUSIVE)
49+
{
50+
Some((trim_span(cx.sess().source_map(), start.between(end)), "..="))
51+
} else {
52+
None
53+
};
54+
check_range(cx, e.span, start, end, sugg);
55+
}
56+
}
57+
58+
fn check_pat(&mut self, cx: &EarlyContext<'_>, p: &Pat) {
59+
if let PatKind::Range(Some(start), Some(end), kind) = &p.kind
60+
&& matches!(kind.node, RangeEnd::Excluded)
61+
{
62+
let sugg = if meets_msrv(self.msrv, msrvs::RANGE_INCLUSIVE) {
63+
"..="
64+
} else {
65+
"..."
66+
};
67+
check_range(cx, p.span, start, end, Some((kind.span, sugg)));
68+
}
69+
}
70+
71+
extract_msrv_attr!(EarlyContext);
72+
}
73+
74+
fn check_range(cx: &EarlyContext<'_>, span: Span, start: &Expr, end: &Expr, sugg: Option<(Span, &str)>) {
75+
if let ExprKind::Lit(start_lit) = &start.peel_parens().kind
76+
&& let ExprKind::Lit(end_lit) = &end.peel_parens().kind
77+
&& matches!(
78+
(&start_lit.kind, &end_lit.kind),
79+
(LitKind::Byte(b'a') | LitKind::Char('a'), LitKind::Byte(b'z') | LitKind::Char('z'))
80+
| (LitKind::Byte(b'A') | LitKind::Char('A'), LitKind::Byte(b'Z') | LitKind::Char('Z'))
81+
)
82+
{
83+
span_lint_and_then(
84+
cx,
85+
ALMOST_COMPLETE_LETTER_RANGE,
86+
span,
87+
"almost complete ascii letter range",
88+
|diag| {
89+
if let Some((span, sugg)) = sugg {
90+
diag.span_suggestion(
91+
span,
92+
"use an inclusive range",
93+
sugg.to_owned(),
94+
Applicability::MaybeIncorrect,
95+
);
96+
}
97+
}
98+
);
99+
}
100+
}

clippy_lints/src/lib.register_all.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
store.register_group(true, "clippy::all", Some("clippy_all"), vec![
66
LintId::of(absurd_extreme_comparisons::ABSURD_EXTREME_COMPARISONS),
7+
LintId::of(almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE),
78
LintId::of(approx_const::APPROX_CONSTANT),
89
LintId::of(assertions_on_constants::ASSERTIONS_ON_CONSTANTS),
910
LintId::of(assign_ops::ASSIGN_OP_PATTERN),

clippy_lints/src/lib.register_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ store.register_lints(&[
3434
#[cfg(feature = "internal")]
3535
utils::internal_lints::UNNECESSARY_SYMBOL_STR,
3636
absurd_extreme_comparisons::ABSURD_EXTREME_COMPARISONS,
37+
almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE,
3738
approx_const::APPROX_CONSTANT,
3839
arithmetic::FLOAT_ARITHMETIC,
3940
arithmetic::INTEGER_ARITHMETIC,

clippy_lints/src/lib.register_suspicious.rs

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Manual edits will be overwritten.
44

55
store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), vec![
6+
LintId::of(almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE),
67
LintId::of(assign_ops::MISREFACTORED_ASSIGN_OP),
78
LintId::of(attrs::BLANKET_CLIPPY_RESTRICTION_LINTS),
89
LintId::of(await_holding_invalid::AWAIT_HOLDING_INVALID_TYPE),

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ mod renamed_lints;
168168

169169
// begin lints modules, do not remove this comment, it’s used in `update_lints`
170170
mod absurd_extreme_comparisons;
171+
mod almost_complete_letter_range;
171172
mod approx_const;
172173
mod arithmetic;
173174
mod as_conversions;
@@ -911,6 +912,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
911912
store.register_early_pass(|| Box::new(duplicate_mod::DuplicateMod::default()));
912913
store.register_late_pass(|| Box::new(get_first::GetFirst));
913914
store.register_early_pass(|| Box::new(unused_rounding::UnusedRounding));
915+
store.register_early_pass(move || Box::new(almost_complete_letter_range::AlmostCompleteLetterRange::new(msrv)));
914916
// add lints here, do not remove this comment, it's used in `new_lint`
915917
}
916918

clippy_utils/src/msrvs.rs

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ msrv_aliases! {
3030
1,34,0 { TRY_FROM }
3131
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
3232
1,28,0 { FROM_BOOL }
33+
1,26,0 { RANGE_INCLUSIVE }
3334
1,17,0 { FIELD_INIT_SHORTHAND, STATIC_IN_CONST, EXPECT_ERR }
3435
1,16,0 { STR_REPEAT }
3536
1,24,0 { IS_ASCII_DIGIT }

clippy_utils/src/source.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use rustc_hir::{Expr, ExprKind};
88
use rustc_lint::{LateContext, LintContext};
99
use rustc_span::hygiene;
1010
use rustc_span::source_map::SourceMap;
11-
use rustc_span::{BytePos, Pos, Span, SyntaxContext};
11+
use rustc_span::{BytePos, Pos, Span, SpanData, SyntaxContext};
1212
use std::borrow::Cow;
1313

1414
/// Checks if the span starts with the given text. This will return false if the span crosses
@@ -389,6 +389,27 @@ pub fn without_block_comments(lines: Vec<&str>) -> Vec<&str> {
389389
without
390390
}
391391

392+
/// Trims the whitespace from the start and the end of the span.
393+
pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
394+
let data = span.data();
395+
let sf: &_ = &sm.lookup_source_file(data.lo);
396+
let Some(src) = sf.src.as_deref() else {
397+
return span;
398+
};
399+
let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
400+
return span;
401+
};
402+
let trim_start = snip.len() - snip.trim_start().len();
403+
let trim_end = snip.len() - snip.trim_end().len();
404+
SpanData {
405+
lo: data.lo + BytePos::from_usize(trim_start),
406+
hi: data.hi - BytePos::from_usize(trim_end),
407+
ctxt: data.ctxt,
408+
parent: data.parent,
409+
}
410+
.span()
411+
}
412+
392413
#[cfg(test)]
393414
mod test {
394415
use super::{reindent_multiline, without_block_comments};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// run-rustfix
2+
// edition:2018
3+
4+
#![feature(custom_inner_attributes)]
5+
#![feature(exclusive_range_pattern)]
6+
#![feature(stmt_expr_attributes)]
7+
#![warn(clippy::almost_complete_letter_range)]
8+
#![allow(ellipsis_inclusive_range_patterns)]
9+
10+
macro_rules! a {
11+
() => {
12+
'a'
13+
};
14+
}
15+
16+
fn main() {
17+
#[rustfmt::skip]
18+
{
19+
let _ = ('a') ..='z';
20+
let _ = 'A' ..= ('Z');
21+
}
22+
23+
let _ = 'b'..'z';
24+
let _ = 'B'..'Z';
25+
26+
let _ = (b'a')..=(b'z');
27+
let _ = b'A'..=b'Z';
28+
29+
let _ = b'b'..b'z';
30+
let _ = b'B'..b'Z';
31+
32+
let _ = a!()..='z';
33+
34+
let _ = match 0u8 {
35+
b'a'..=b'z' if true => 1,
36+
b'A'..=b'Z' if true => 2,
37+
b'b'..b'z' => 3,
38+
b'B'..b'Z' => 4,
39+
_ => 5,
40+
};
41+
42+
let _ = match 'x' {
43+
'a'..='z' if true => 1,
44+
'A'..='Z' if true => 2,
45+
'b'..'z' => 3,
46+
'B'..'Z' => 4,
47+
_ => 5,
48+
};
49+
}
50+
51+
fn _under_msrv() {
52+
#![clippy::msrv = "1.25"]
53+
let _ = match 'a' {
54+
'a'...'z' => 1,
55+
_ => 2,
56+
};
57+
}
58+
59+
fn _meets_msrv() {
60+
#![clippy::msrv = "1.26"]
61+
let _ = 'a'..='z';
62+
let _ = match 'a' {
63+
'a'..='z' => 1,
64+
_ => 2,
65+
};
66+
}
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// run-rustfix
2+
// edition:2018
3+
4+
#![feature(custom_inner_attributes)]
5+
#![feature(exclusive_range_pattern)]
6+
#![feature(stmt_expr_attributes)]
7+
#![warn(clippy::almost_complete_letter_range)]
8+
#![allow(ellipsis_inclusive_range_patterns)]
9+
10+
macro_rules! a {
11+
() => {
12+
'a'
13+
};
14+
}
15+
16+
fn main() {
17+
#[rustfmt::skip]
18+
{
19+
let _ = ('a') ..'z';
20+
let _ = 'A' .. ('Z');
21+
}
22+
23+
let _ = 'b'..'z';
24+
let _ = 'B'..'Z';
25+
26+
let _ = (b'a')..(b'z');
27+
let _ = b'A'..b'Z';
28+
29+
let _ = b'b'..b'z';
30+
let _ = b'B'..b'Z';
31+
32+
let _ = a!()..'z';
33+
34+
let _ = match 0u8 {
35+
b'a'..b'z' if true => 1,
36+
b'A'..b'Z' if true => 2,
37+
b'b'..b'z' => 3,
38+
b'B'..b'Z' => 4,
39+
_ => 5,
40+
};
41+
42+
let _ = match 'x' {
43+
'a'..'z' if true => 1,
44+
'A'..'Z' if true => 2,
45+
'b'..'z' => 3,
46+
'B'..'Z' => 4,
47+
_ => 5,
48+
};
49+
}
50+
51+
fn _under_msrv() {
52+
#![clippy::msrv = "1.25"]
53+
let _ = match 'a' {
54+
'a'..'z' => 1,
55+
_ => 2,
56+
};
57+
}
58+
59+
fn _meets_msrv() {
60+
#![clippy::msrv = "1.26"]
61+
let _ = 'a'..'z';
62+
let _ = match 'a' {
63+
'a'..'z' => 1,
64+
_ => 2,
65+
};
66+
}

0 commit comments

Comments
 (0)