Skip to content

Commit 708a818

Browse files
authored
Merge pull request #2057 from topecongiro/issue-1818
Enhance CHARS_*_CMP lint
2 parents 013b1f5 + d5d300c commit 708a818

File tree

3 files changed

+248
-13
lines changed

3 files changed

+248
-13
lines changed

clippy_lints/src/methods.rs

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use rustc::ty::subst::Substs;
77
use rustc_const_eval::ConstContext;
88
use std::borrow::Cow;
99
use std::fmt;
10+
use syntax::ast;
1011
use syntax::codemap::Span;
1112
use utils::{get_trait_def_id, implements_trait, in_external_macro, in_macro, is_copy, is_self, is_self_ty,
1213
iter_input_pats, last_path_segment, match_def_path, match_path, match_qpath, match_trait_method,
@@ -544,6 +545,24 @@ declare_lint! {
544545
"using `.cloned().collect()` on slice to create a `Vec`"
545546
}
546547

548+
/// **What it does:** Checks for usage of `.chars().last()` or
549+
/// `.chars().next_back()` on a `str` to check if it ends with a given char.
550+
///
551+
/// **Why is this bad?** Readability, this can be written more concisely as
552+
/// `_.ends_with(_)`.
553+
///
554+
/// **Known problems:** None.
555+
///
556+
/// **Example:**
557+
/// ```rust
558+
/// name.chars().last() == Some('_') || name.chars().next_back() == Some('-')
559+
/// ```
560+
declare_lint! {
561+
pub CHARS_LAST_CMP,
562+
Warn,
563+
"using `.chars().last()` or `.chars().next_back()` to check if a string ends with a char"
564+
}
565+
547566
impl LintPass for Pass {
548567
fn get_lints(&self) -> LintArray {
549568
lint_array!(
@@ -557,6 +576,7 @@ impl LintPass for Pass {
557576
OPTION_MAP_UNWRAP_OR_ELSE,
558577
OR_FUN_CALL,
559578
CHARS_NEXT_CMP,
579+
CHARS_LAST_CMP,
560580
CLONE_ON_COPY,
561581
CLONE_ON_REF_PTR,
562582
CLONE_DOUBLE_REF,
@@ -648,9 +668,13 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for Pass {
648668
}
649669
},
650670
hir::ExprBinary(op, ref lhs, ref rhs) if op.node == hir::BiEq || op.node == hir::BiNe => {
651-
if !lint_chars_next(cx, expr, lhs, rhs, op.node == hir::BiEq) {
652-
lint_chars_next(cx, expr, rhs, lhs, op.node == hir::BiEq);
653-
}
671+
let mut info = BinaryExprInfo {
672+
expr: expr,
673+
chain: lhs,
674+
other: rhs,
675+
eq: op.node == hir::BiEq,
676+
};
677+
lint_binary_expr_with_method_call(cx, &mut info);
654678
},
655679
_ => (),
656680
}
@@ -1285,11 +1309,39 @@ fn lint_search_is_some<'a, 'tcx>(
12851309
}
12861310
}
12871311

1288-
/// Checks for the `CHARS_NEXT_CMP` lint.
1289-
fn lint_chars_next<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &'tcx hir::Expr, chain: &'tcx hir::Expr, other: &'tcx hir::Expr, eq: bool) -> bool {
1312+
/// Used for `lint_binary_expr_with_method_call`.
1313+
#[derive(Copy, Clone)]
1314+
struct BinaryExprInfo<'a> {
1315+
expr: &'a hir::Expr,
1316+
chain: &'a hir::Expr,
1317+
other: &'a hir::Expr,
1318+
eq: bool,
1319+
}
1320+
1321+
/// Checks for the `CHARS_NEXT_CMP` and `CHARS_LAST_CMP` lints.
1322+
fn lint_binary_expr_with_method_call<'a, 'tcx: 'a>(cx: &LateContext<'a, 'tcx>, info: &mut BinaryExprInfo) {
1323+
macro_rules! lint_with_both_lhs_and_rhs {
1324+
($func:ident, $cx:expr, $info:ident) => {
1325+
if !$func($cx, $info) {
1326+
::std::mem::swap(&mut $info.chain, &mut $info.other);
1327+
if $func($cx, $info) {
1328+
return;
1329+
}
1330+
}
1331+
}
1332+
}
1333+
1334+
lint_with_both_lhs_and_rhs!(lint_chars_next_cmp, cx, info);
1335+
lint_with_both_lhs_and_rhs!(lint_chars_last_cmp, cx, info);
1336+
lint_with_both_lhs_and_rhs!(lint_chars_next_cmp_with_unwrap, cx, info);
1337+
lint_with_both_lhs_and_rhs!(lint_chars_last_cmp_with_unwrap, cx, info);
1338+
}
1339+
1340+
/// Wrapper fn for `CHARS_NEXT_CMP` and `CHARS_NEXT_CMP` lints.
1341+
fn lint_chars_cmp<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo, chain_methods: &[&str], lint: &'static Lint, suggest: &str) -> bool {
12901342
if_let_chain! {[
1291-
let Some(args) = method_chain_args(chain, &["chars", "next"]),
1292-
let hir::ExprCall(ref fun, ref arg_char) = other.node,
1343+
let Some(args) = method_chain_args(info.chain, chain_methods),
1344+
let hir::ExprCall(ref fun, ref arg_char) = info.other.node,
12931345
arg_char.len() == 1,
12941346
let hir::ExprPath(ref qpath) = fun.node,
12951347
let Some(segment) = single_segment_path(qpath),
@@ -1302,13 +1354,14 @@ fn lint_chars_next<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &'tcx hir::Expr,
13021354
}
13031355

13041356
span_lint_and_sugg(cx,
1305-
CHARS_NEXT_CMP,
1306-
expr.span,
1307-
"you should use the `starts_with` method",
1357+
lint,
1358+
info.expr.span,
1359+
&format!("you should use the `{}` method", suggest),
13081360
"like this",
1309-
format!("{}{}.starts_with({})",
1310-
if eq { "" } else { "!" },
1361+
format!("{}{}.{}({})",
1362+
if info.eq { "" } else { "!" },
13111363
snippet(cx, args[0][0].span, "_"),
1364+
suggest,
13121365
snippet(cx, arg_char[0].span, "_")));
13131366

13141367
return true;
@@ -1317,6 +1370,60 @@ fn lint_chars_next<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &'tcx hir::Expr,
13171370
false
13181371
}
13191372

1373+
/// Checks for the `CHARS_NEXT_CMP` lint.
1374+
fn lint_chars_next_cmp<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo) -> bool {
1375+
lint_chars_cmp(cx, info, &["chars", "next"], CHARS_NEXT_CMP, "starts_with")
1376+
}
1377+
1378+
/// Checks for the `CHARS_LAST_CMP` lint.
1379+
fn lint_chars_last_cmp<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo) -> bool {
1380+
if lint_chars_cmp(cx, info, &["chars", "last"], CHARS_NEXT_CMP, "ends_with") {
1381+
true
1382+
} else {
1383+
lint_chars_cmp(cx, info, &["chars", "next_back"], CHARS_NEXT_CMP, "ends_with")
1384+
}
1385+
}
1386+
1387+
/// Wrapper fn for `CHARS_NEXT_CMP` and `CHARS_LAST_CMP` lints with `unwrap()`.
1388+
fn lint_chars_cmp_with_unwrap<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo, chain_methods: &[&str], lint: &'static Lint, suggest: &str) -> bool {
1389+
if_let_chain! {[
1390+
let Some(args) = method_chain_args(info.chain, chain_methods),
1391+
let hir::ExprLit(ref lit) = info.other.node,
1392+
let ast::LitKind::Char(c) = lit.node,
1393+
], {
1394+
span_lint_and_sugg(
1395+
cx,
1396+
lint,
1397+
info.expr.span,
1398+
&format!("you should use the `{}` method", suggest),
1399+
"like this",
1400+
format!("{}{}.{}('{}')",
1401+
if info.eq { "" } else { "!" },
1402+
snippet(cx, args[0][0].span, "_"),
1403+
suggest,
1404+
c)
1405+
);
1406+
1407+
return true;
1408+
}}
1409+
1410+
false
1411+
}
1412+
1413+
/// Checks for the `CHARS_NEXT_CMP` lint with `unwrap()`.
1414+
fn lint_chars_next_cmp_with_unwrap<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo) -> bool {
1415+
lint_chars_cmp_with_unwrap(cx, info, &["chars", "next", "unwrap"], CHARS_NEXT_CMP, "starts_with")
1416+
}
1417+
1418+
/// Checks for the `CHARS_LAST_CMP` lint with `unwrap()`.
1419+
fn lint_chars_last_cmp_with_unwrap<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo) -> bool {
1420+
if lint_chars_cmp_with_unwrap(cx, info, &["chars", "last", "unwrap"], CHARS_LAST_CMP, "ends_with") {
1421+
true
1422+
} else {
1423+
lint_chars_cmp_with_unwrap(cx, info, &["chars", "next_back", "unwrap"], CHARS_LAST_CMP, "ends_with")
1424+
}
1425+
}
1426+
13201427
/// lint for length-1 `str`s for methods in `PATTERN_METHODS`
13211428
fn lint_single_char_pattern<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &'tcx hir::Expr, arg: &'tcx hir::Expr) {
13221429
let parent_item = cx.tcx.hir.get_parent(arg.id);

tests/ui/methods.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,3 +547,33 @@ fn iter_clone_collect() {
547547
let v3 : HashSet<isize> = v.iter().cloned().collect();
548548
let v4 : VecDeque<isize> = v.iter().cloned().collect();
549549
}
550+
551+
fn chars_cmp_with_unwrap() {
552+
let s = String::from("foo");
553+
if s.chars().next().unwrap() == 'f' { // s.starts_with('f')
554+
// Nothing here
555+
}
556+
if s.chars().next_back().unwrap() == 'o' { // s.ends_with('o')
557+
// Nothing here
558+
}
559+
if s.chars().last().unwrap() == 'o' { // s.ends_with('o')
560+
// Nothing here
561+
}
562+
if s.chars().next().unwrap() != 'f' { // !s.starts_with('f')
563+
// Nothing here
564+
}
565+
if s.chars().next_back().unwrap() != 'o' { // !s.ends_with('o')
566+
// Nothing here
567+
}
568+
if s.chars().last().unwrap() != 'o' { // !s.ends_with('o')
569+
// Nothing here
570+
}
571+
}
572+
573+
#[allow(unnecessary_operation)]
574+
fn ends_with() {
575+
"".chars().last() == Some(' ');
576+
Some(' ') != "".chars().last();
577+
"".chars().next_back() == Some(' ');
578+
Some(' ') != "".chars().next_back();
579+
}

tests/ui/methods.stderr

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,5 +738,103 @@ error: called `cloned().collect()` on a slice to create a `Vec`. Calling `to_vec
738738
|
739739
= note: `-D iter-cloned-collect` implied by `-D warnings`
740740

741-
error: aborting due to 107 previous errors
741+
error: you should use the `starts_with` method
742+
--> $DIR/methods.rs:553:8
743+
|
744+
553 | if s.chars().next().unwrap() == 'f' { // s.starts_with('f')
745+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `s.starts_with('f')`
746+
747+
error: used unwrap() on an Option value. If you don't want to handle the None case gracefully, consider using expect() to provide a better panic message
748+
--> $DIR/methods.rs:553:8
749+
|
750+
553 | if s.chars().next().unwrap() == 'f' { // s.starts_with('f')
751+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
752+
753+
error: you should use the `ends_with` method
754+
--> $DIR/methods.rs:556:8
755+
|
756+
556 | if s.chars().next_back().unwrap() == 'o' { // s.ends_with('o')
757+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `s.ends_with('o')`
758+
|
759+
= note: `-D chars-last-cmp` implied by `-D warnings`
760+
761+
error: used unwrap() on an Option value. If you don't want to handle the None case gracefully, consider using expect() to provide a better panic message
762+
--> $DIR/methods.rs:556:8
763+
|
764+
556 | if s.chars().next_back().unwrap() == 'o' { // s.ends_with('o')
765+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
766+
767+
error: you should use the `ends_with` method
768+
--> $DIR/methods.rs:559:8
769+
|
770+
559 | if s.chars().last().unwrap() == 'o' { // s.ends_with('o')
771+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `s.ends_with('o')`
772+
773+
error: used unwrap() on an Option value. If you don't want to handle the None case gracefully, consider using expect() to provide a better panic message
774+
--> $DIR/methods.rs:559:8
775+
|
776+
559 | if s.chars().last().unwrap() == 'o' { // s.ends_with('o')
777+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
778+
779+
error: you should use the `starts_with` method
780+
--> $DIR/methods.rs:562:8
781+
|
782+
562 | if s.chars().next().unwrap() != 'f' { // !s.starts_with('f')
783+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!s.starts_with('f')`
784+
785+
error: used unwrap() on an Option value. If you don't want to handle the None case gracefully, consider using expect() to provide a better panic message
786+
--> $DIR/methods.rs:562:8
787+
|
788+
562 | if s.chars().next().unwrap() != 'f' { // !s.starts_with('f')
789+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
790+
791+
error: you should use the `ends_with` method
792+
--> $DIR/methods.rs:565:8
793+
|
794+
565 | if s.chars().next_back().unwrap() != 'o' { // !s.ends_with('o')
795+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!s.ends_with('o')`
796+
797+
error: used unwrap() on an Option value. If you don't want to handle the None case gracefully, consider using expect() to provide a better panic message
798+
--> $DIR/methods.rs:565:8
799+
|
800+
565 | if s.chars().next_back().unwrap() != 'o' { // !s.ends_with('o')
801+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
802+
803+
error: you should use the `ends_with` method
804+
--> $DIR/methods.rs:568:8
805+
|
806+
568 | if s.chars().last().unwrap() != 'o' { // !s.ends_with('o')
807+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!s.ends_with('o')`
808+
809+
error: used unwrap() on an Option value. If you don't want to handle the None case gracefully, consider using expect() to provide a better panic message
810+
--> $DIR/methods.rs:568:8
811+
|
812+
568 | if s.chars().last().unwrap() != 'o' { // !s.ends_with('o')
813+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
814+
815+
error: you should use the `ends_with` method
816+
--> $DIR/methods.rs:575:5
817+
|
818+
575 | "".chars().last() == Some(' ');
819+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `"".ends_with(' ')`
820+
821+
error: you should use the `ends_with` method
822+
--> $DIR/methods.rs:576:5
823+
|
824+
576 | Some(' ') != "".chars().last();
825+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!"".ends_with(' ')`
826+
827+
error: you should use the `ends_with` method
828+
--> $DIR/methods.rs:577:5
829+
|
830+
577 | "".chars().next_back() == Some(' ');
831+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `"".ends_with(' ')`
832+
833+
error: you should use the `ends_with` method
834+
--> $DIR/methods.rs:578:5
835+
|
836+
578 | Some(' ') != "".chars().next_back();
837+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!"".ends_with(' ')`
838+
839+
error: aborting due to 123 previous errors
742840

0 commit comments

Comments
 (0)