about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJason Newcomb <jsnewcomb@pm.me>2022-05-30 22:07:49 -0400
committerJason Newcomb <jsnewcomb@pm.me>2022-05-30 23:20:04 -0400
commiteb2908b4ea8fd63623f994780d2928f32af84d71 (patch)
tree1b0312875c5390a960486ab0906f023ff72327a0
parent39231b4b503955626c1ccf965b8328ac67c523bb (diff)
downloadrust-eb2908b4ea8fd63623f994780d2928f32af84d71.tar.gz
rust-eb2908b4ea8fd63623f994780d2928f32af84d71.zip
Add lint `almost_complete_letter_range`
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/almost_complete_letter_range.rs100
-rw-r--r--clippy_lints/src/lib.register_all.rs1
-rw-r--r--clippy_lints/src/lib.register_lints.rs1
-rw-r--r--clippy_lints/src/lib.register_suspicious.rs1
-rw-r--r--clippy_lints/src/lib.rs2
-rw-r--r--clippy_utils/src/msrvs.rs1
-rw-r--r--clippy_utils/src/source.rs23
-rw-r--r--tests/ui/almost_complete_letter_range.fixed66
-rw-r--r--tests/ui/almost_complete_letter_range.rs66
-rw-r--r--tests/ui/almost_complete_letter_range.stderr100
11 files changed, 361 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3aaa79a1719..67ca4cf708a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
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 00000000000..b364a370efa
--- /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<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,
+                    );
+                }
+            }
+        );
+    }
+}
diff --git a/clippy_lints/src/lib.register_all.rs b/clippy_lints/src/lib.register_all.rs
index a028b41db77..d4f8d676c22 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 1f2132cf620..b76eaae377e 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 20bf5a245b1..3d6d2d8951c 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 d611600cf71..67ae2053f5a 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 134fd1ce505..b9ec2c19fdd 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 04ef2f57447..f88a92fb11c 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 00000000000..39f8f0c2949
--- /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 00000000000..3dc02199257
--- /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 00000000000..74980ec1a92
--- /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
+