Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lint almost_complete_letter_range #8918

Merged
merged 1 commit into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3276,6 +3276,7 @@ Released 2018-09-13
<!-- begin autogenerated links to lint list -->
[`absurd_extreme_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#absurd_extreme_comparisons
[`allow_attributes_without_reason`]: https://rust-lang.github.io/rust-clippy/master/index.html#allow_attributes_without_reason
[`almost_complete_letter_range`]: https://rust-lang.github.io/rust-clippy/master/index.html#almost_complete_letter_range
[`almost_swapped`]: https://rust-lang.github.io/rust-clippy/master/index.html#almost_swapped
[`approx_constant`]: https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
[`as_conversions`]: https://rust-lang.github.io/rust-clippy/master/index.html#as_conversions
Expand Down
100 changes: 100 additions & 0 deletions clippy_lints/src/almost_complete_letter_range.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::{trim_span, walk_span_to_context};
use clippy_utils::{meets_msrv, msrvs};
use rustc_ast::ast::{Expr, ExprKind, LitKind, Pat, PatKind, RangeEnd, RangeLimits};
use rustc_errors::Applicability;
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
use rustc_semver::RustcVersion;
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::Span;

declare_clippy_lint! {
/// ### What it does
/// Checks for ranges which almost include the entire range of letters from 'a' to 'z', but
/// don't because they're a half open range.
///
/// ### Why is this bad?
/// This (`'a'..'z'`) is almost certainly a typo meant to include all letters.
///
/// ### Example
/// ```rust
/// let _ = 'a'..'z';
/// ```
/// Use instead:
/// ```rust
/// let _ = 'a'..='z';
/// ```
#[clippy::version = "1.63.0"]
pub ALMOST_COMPLETE_LETTER_RANGE,
suspicious,
"almost complete letter range"
}
impl_lint_pass!(AlmostCompleteLetterRange => [ALMOST_COMPLETE_LETTER_RANGE]);

pub struct AlmostCompleteLetterRange {
msrv: Option<RustcVersion>,
}
impl AlmostCompleteLetterRange {
pub fn new(msrv: Option<RustcVersion>) -> Self {
Self { msrv }
}
}
impl EarlyLintPass for AlmostCompleteLetterRange {
fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &Expr) {
if let ExprKind::Range(Some(start), Some(end), RangeLimits::HalfOpen) = &e.kind {
let ctxt = e.span.ctxt();
let sugg = if let Some(start) = walk_span_to_context(start.span, ctxt)
&& let Some(end) = walk_span_to_context(end.span, ctxt)
&& meets_msrv(self.msrv, msrvs::RANGE_INCLUSIVE)
{
Some((trim_span(cx.sess().source_map(), start.between(end)), "..="))
} else {
None
};
check_range(cx, e.span, start, end, sugg);
}
}

fn check_pat(&mut self, cx: &EarlyContext<'_>, p: &Pat) {
if let PatKind::Range(Some(start), Some(end), kind) = &p.kind
&& matches!(kind.node, RangeEnd::Excluded)
{
let sugg = if meets_msrv(self.msrv, msrvs::RANGE_INCLUSIVE) {
"..="
} else {
"..."
};
check_range(cx, p.span, start, end, Some((kind.span, sugg)));
}
}

extract_msrv_attr!(EarlyContext);
}

fn check_range(cx: &EarlyContext<'_>, span: Span, start: &Expr, end: &Expr, sugg: Option<(Span, &str)>) {
if let ExprKind::Lit(start_lit) = &start.peel_parens().kind
&& let ExprKind::Lit(end_lit) = &end.peel_parens().kind
&& matches!(
(&start_lit.kind, &end_lit.kind),
(LitKind::Byte(b'a') | LitKind::Char('a'), LitKind::Byte(b'z') | LitKind::Char('z'))
| (LitKind::Byte(b'A') | LitKind::Char('A'), LitKind::Byte(b'Z') | LitKind::Char('Z'))
)
{
span_lint_and_then(
cx,
ALMOST_COMPLETE_LETTER_RANGE,
span,
"almost complete ascii letter range",
|diag| {
if let Some((span, sugg)) = sugg {
diag.span_suggestion(
span,
"use an inclusive range",
sugg.to_owned(),
Applicability::MaybeIncorrect,
);
}
}
);
}
}
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

store.register_group(true, "clippy::all", Some("clippy_all"), vec![
LintId::of(absurd_extreme_comparisons::ABSURD_EXTREME_COMPARISONS),
LintId::of(almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE),
LintId::of(approx_const::APPROX_CONSTANT),
LintId::of(assertions_on_constants::ASSERTIONS_ON_CONSTANTS),
LintId::of(assign_ops::ASSIGN_OP_PATTERN),
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ store.register_lints(&[
#[cfg(feature = "internal")]
utils::internal_lints::UNNECESSARY_SYMBOL_STR,
absurd_extreme_comparisons::ABSURD_EXTREME_COMPARISONS,
almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE,
approx_const::APPROX_CONSTANT,
arithmetic::FLOAT_ARITHMETIC,
arithmetic::INTEGER_ARITHMETIC,
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_suspicious.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Manual edits will be overwritten.

store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), vec![
LintId::of(almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE),
LintId::of(assign_ops::MISREFACTORED_ASSIGN_OP),
LintId::of(attrs::BLANKET_CLIPPY_RESTRICTION_LINTS),
LintId::of(await_holding_invalid::AWAIT_HOLDING_INVALID_TYPE),
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ mod renamed_lints;

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

Expand Down
1 change: 1 addition & 0 deletions clippy_utils/src/msrvs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ msrv_aliases! {
1,34,0 { TRY_FROM }
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
1,28,0 { FROM_BOOL }
1,26,0 { RANGE_INCLUSIVE }
1,17,0 { FIELD_INIT_SHORTHAND, STATIC_IN_CONST, EXPECT_ERR }
1,16,0 { STR_REPEAT }
1,24,0 { IS_ASCII_DIGIT }
Expand Down
23 changes: 22 additions & 1 deletion clippy_utils/src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LintContext};
use rustc_span::hygiene;
use rustc_span::source_map::SourceMap;
use rustc_span::{BytePos, Pos, Span, SyntaxContext};
use rustc_span::{BytePos, Pos, Span, SpanData, SyntaxContext};
use std::borrow::Cow;

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

/// Trims the whitespace from the start and the end of the span.
pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
let data = span.data();
let sf: &_ = &sm.lookup_source_file(data.lo);
let Some(src) = sf.src.as_deref() else {
return span;
};
let Some(snip) = &src.get((data.lo - sf.start_pos).to_usize()..(data.hi - sf.start_pos).to_usize()) else {
return span;
};
let trim_start = snip.len() - snip.trim_start().len();
let trim_end = snip.len() - snip.trim_end().len();
SpanData {
lo: data.lo + BytePos::from_usize(trim_start),
hi: data.hi - BytePos::from_usize(trim_end),
ctxt: data.ctxt,
parent: data.parent,
}
.span()
}

#[cfg(test)]
mod test {
use super::{reindent_multiline, without_block_comments};
Expand Down
66 changes: 66 additions & 0 deletions tests/ui/almost_complete_letter_range.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// run-rustfix
// edition:2018

#![feature(custom_inner_attributes)]
#![feature(exclusive_range_pattern)]
#![feature(stmt_expr_attributes)]
#![warn(clippy::almost_complete_letter_range)]
#![allow(ellipsis_inclusive_range_patterns)]

macro_rules! a {
() => {
'a'
};
}

fn main() {
#[rustfmt::skip]
{
let _ = ('a') ..='z';
let _ = 'A' ..= ('Z');
}

let _ = 'b'..'z';
let _ = 'B'..'Z';

let _ = (b'a')..=(b'z');
let _ = b'A'..=b'Z';

let _ = b'b'..b'z';
let _ = b'B'..b'Z';

let _ = a!()..='z';

let _ = match 0u8 {
b'a'..=b'z' if true => 1,
b'A'..=b'Z' if true => 2,
b'b'..b'z' => 3,
b'B'..b'Z' => 4,
_ => 5,
};

let _ = match 'x' {
'a'..='z' if true => 1,
'A'..='Z' if true => 2,
'b'..'z' => 3,
'B'..'Z' => 4,
_ => 5,
};
}

fn _under_msrv() {
#![clippy::msrv = "1.25"]
let _ = match 'a' {
'a'...'z' => 1,
_ => 2,
};
}

fn _meets_msrv() {
#![clippy::msrv = "1.26"]
let _ = 'a'..='z';
let _ = match 'a' {
'a'..='z' => 1,
_ => 2,
};
}
66 changes: 66 additions & 0 deletions tests/ui/almost_complete_letter_range.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// run-rustfix
// edition:2018

#![feature(custom_inner_attributes)]
#![feature(exclusive_range_pattern)]
#![feature(stmt_expr_attributes)]
#![warn(clippy::almost_complete_letter_range)]
#![allow(ellipsis_inclusive_range_patterns)]

macro_rules! a {
() => {
'a'
};
}

fn main() {
#[rustfmt::skip]
{
let _ = ('a') ..'z';
let _ = 'A' .. ('Z');
}

let _ = 'b'..'z';
let _ = 'B'..'Z';

let _ = (b'a')..(b'z');
let _ = b'A'..b'Z';

let _ = b'b'..b'z';
let _ = b'B'..b'Z';

let _ = a!()..'z';

let _ = match 0u8 {
b'a'..b'z' if true => 1,
b'A'..b'Z' if true => 2,
b'b'..b'z' => 3,
b'B'..b'Z' => 4,
_ => 5,
};

let _ = match 'x' {
'a'..'z' if true => 1,
'A'..'Z' if true => 2,
'b'..'z' => 3,
'B'..'Z' => 4,
_ => 5,
};
}

fn _under_msrv() {
#![clippy::msrv = "1.25"]
let _ = match 'a' {
'a'..'z' => 1,
_ => 2,
};
}

fn _meets_msrv() {
#![clippy::msrv = "1.26"]
let _ = 'a'..'z';
let _ = match 'a' {
'a'..'z' => 1,
_ => 2,
};
}
Loading