diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aaa79a1719c2..67ca4cf708aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3276,6 +3276,7 @@ Released 2018-09-13 [`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 diff --git a/clippy_lints/src/almost_complete_letter_range.rs b/clippy_lints/src/almost_complete_letter_range.rs new file mode 100644 index 0000000000000..b364a370efab5 --- /dev/null +++ b/clippy_lints/src/almost_complete_letter_range.rs @@ -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, +} +impl AlmostCompleteLetterRange { + pub fn new(msrv: Option) -> 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, + ); + } + } + ); + } +} diff --git a/clippy_lints/src/lib.register_all.rs b/clippy_lints/src/lib.register_all.rs index a028b41db7740..d4f8d676c22d0 100644 --- a/clippy_lints/src/lib.register_all.rs +++ b/clippy_lints/src/lib.register_all.rs @@ -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), diff --git a/clippy_lints/src/lib.register_lints.rs b/clippy_lints/src/lib.register_lints.rs index 1f2132cf62003..b76eaae377e32 100644 --- a/clippy_lints/src/lib.register_lints.rs +++ b/clippy_lints/src/lib.register_lints.rs @@ -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, diff --git a/clippy_lints/src/lib.register_suspicious.rs b/clippy_lints/src/lib.register_suspicious.rs index 20bf5a245b157..3d6d2d8951c23 100644 --- a/clippy_lints/src/lib.register_suspicious.rs +++ b/clippy_lints/src/lib.register_suspicious.rs @@ -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), diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index d611600cf711b..67ae2053f5ae0 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -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; @@ -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` } diff --git a/clippy_utils/src/msrvs.rs b/clippy_utils/src/msrvs.rs index 134fd1ce505a0..b9ec2c19fdd31 100644 --- a/clippy_utils/src/msrvs.rs +++ b/clippy_utils/src/msrvs.rs @@ -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 } diff --git a/clippy_utils/src/source.rs b/clippy_utils/src/source.rs index 04ef2f57447c6..f88a92fb11c11 100644 --- a/clippy_utils/src/source.rs +++ b/clippy_utils/src/source.rs @@ -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 @@ -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}; diff --git a/tests/ui/almost_complete_letter_range.fixed b/tests/ui/almost_complete_letter_range.fixed new file mode 100644 index 0000000000000..39f8f0c29495b --- /dev/null +++ b/tests/ui/almost_complete_letter_range.fixed @@ -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, + }; +} diff --git a/tests/ui/almost_complete_letter_range.rs b/tests/ui/almost_complete_letter_range.rs new file mode 100644 index 0000000000000..3dc0219925760 --- /dev/null +++ b/tests/ui/almost_complete_letter_range.rs @@ -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, + }; +} diff --git a/tests/ui/almost_complete_letter_range.stderr b/tests/ui/almost_complete_letter_range.stderr new file mode 100644 index 0000000000000..74980ec1a923f --- /dev/null +++ b/tests/ui/almost_complete_letter_range.stderr @@ -0,0 +1,100 @@ +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:19:17 + | +LL | let _ = ('a') ..'z'; + | ^^^^^^--^^^ + | | + | help: use an inclusive range: `..=` + | + = note: `-D clippy::almost-complete-letter-range` implied by `-D warnings` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:20:17 + | +LL | let _ = 'A' .. ('Z'); + | ^^^^--^^^^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:26:13 + | +LL | let _ = (b'a')..(b'z'); + | ^^^^^^--^^^^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:27:13 + | +LL | let _ = b'A'..b'Z'; + | ^^^^--^^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:32:13 + | +LL | let _ = a!()..'z'; + | ^^^^--^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:35:9 + | +LL | b'a'..b'z' if true => 1, + | ^^^^--^^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:36:9 + | +LL | b'A'..b'Z' if true => 2, + | ^^^^--^^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:43:9 + | +LL | 'a'..'z' if true => 1, + | ^^^--^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:44:9 + | +LL | 'A'..'Z' if true => 2, + | ^^^--^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:54:9 + | +LL | 'a'..'z' => 1, + | ^^^--^^^ + | | + | help: use an inclusive range: `...` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:61:13 + | +LL | let _ = 'a'..'z'; + | ^^^--^^^ + | | + | help: use an inclusive range: `..=` + +error: almost complete ascii letter range + --> $DIR/almost_complete_letter_range.rs:63:9 + | +LL | 'a'..'z' => 1, + | ^^^--^^^ + | | + | help: use an inclusive range: `..=` + +error: aborting due to 12 previous errors +