Skip to content

Enhance CHARS_*_CMP lint #2057

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

Merged
merged 2 commits into from
Sep 16, 2017
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
131 changes: 119 additions & 12 deletions clippy_lints/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use rustc::ty::subst::Substs;
use rustc_const_eval::ConstContext;
use std::borrow::Cow;
use std::fmt;
use syntax::ast;
use syntax::codemap::Span;
use utils::{get_trait_def_id, implements_trait, in_external_macro, in_macro, is_copy, is_self, is_self_ty,
iter_input_pats, last_path_segment, match_def_path, match_path, match_qpath, match_trait_method,
Expand Down Expand Up @@ -544,6 +545,24 @@ declare_lint! {
"using `.cloned().collect()` on slice to create a `Vec`"
}

/// **What it does:** Checks for usage of `.chars().last()` or
/// `.chars().next_back()` on a `str` to check if it ends with a given char.
///
/// **Why is this bad?** Readability, this can be written more concisely as
/// `_.ends_with(_)`.
///
/// **Known problems:** None.
///
/// **Example:**
/// ```rust
/// name.chars().last() == Some('_') || name.chars().next_back() == Some('-')
/// ```
declare_lint! {
pub CHARS_LAST_CMP,
Warn,
"using `.chars().last()` or `.chars().next_back()` to check if a string ends with a char"
}

impl LintPass for Pass {
fn get_lints(&self) -> LintArray {
lint_array!(
Expand All @@ -557,6 +576,7 @@ impl LintPass for Pass {
OPTION_MAP_UNWRAP_OR_ELSE,
OR_FUN_CALL,
CHARS_NEXT_CMP,
CHARS_LAST_CMP,
CLONE_ON_COPY,
CLONE_ON_REF_PTR,
CLONE_DOUBLE_REF,
Expand Down Expand Up @@ -648,9 +668,13 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for Pass {
}
},
hir::ExprBinary(op, ref lhs, ref rhs) if op.node == hir::BiEq || op.node == hir::BiNe => {
if !lint_chars_next(cx, expr, lhs, rhs, op.node == hir::BiEq) {
lint_chars_next(cx, expr, rhs, lhs, op.node == hir::BiEq);
}
let mut info = BinaryExprInfo {
expr: expr,
chain: lhs,
other: rhs,
eq: op.node == hir::BiEq,
};
lint_binary_expr_with_method_call(cx, &mut info);
},
_ => (),
}
Expand Down Expand Up @@ -1285,11 +1309,39 @@ fn lint_search_is_some<'a, 'tcx>(
}
}

/// Checks for the `CHARS_NEXT_CMP` lint.
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 {
/// Used for `lint_binary_expr_with_method_call`.
#[derive(Copy, Clone)]
struct BinaryExprInfo<'a> {
expr: &'a hir::Expr,
chain: &'a hir::Expr,
other: &'a hir::Expr,
eq: bool,
}

/// Checks for the `CHARS_NEXT_CMP` and `CHARS_LAST_CMP` lints.
fn lint_binary_expr_with_method_call<'a, 'tcx: 'a>(cx: &LateContext<'a, 'tcx>, info: &mut BinaryExprInfo) {
macro_rules! lint_with_both_lhs_and_rhs {
($func:ident, $cx:expr, $info:ident) => {
if !$func($cx, $info) {
::std::mem::swap(&mut $info.chain, &mut $info.other);
if $func($cx, $info) {
return;
}
}
}
}

lint_with_both_lhs_and_rhs!(lint_chars_next_cmp, cx, info);
lint_with_both_lhs_and_rhs!(lint_chars_last_cmp, cx, info);
lint_with_both_lhs_and_rhs!(lint_chars_next_cmp_with_unwrap, cx, info);
lint_with_both_lhs_and_rhs!(lint_chars_last_cmp_with_unwrap, cx, info);
}

/// Wrapper fn for `CHARS_NEXT_CMP` and `CHARS_NEXT_CMP` lints.
fn lint_chars_cmp<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo, chain_methods: &[&str], lint: &'static Lint, suggest: &str) -> bool {
if_let_chain! {[
let Some(args) = method_chain_args(chain, &["chars", "next"]),
let hir::ExprCall(ref fun, ref arg_char) = other.node,
let Some(args) = method_chain_args(info.chain, chain_methods),
let hir::ExprCall(ref fun, ref arg_char) = info.other.node,
arg_char.len() == 1,
let hir::ExprPath(ref qpath) = fun.node,
let Some(segment) = single_segment_path(qpath),
Expand All @@ -1302,13 +1354,14 @@ fn lint_chars_next<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &'tcx hir::Expr,
}

span_lint_and_sugg(cx,
CHARS_NEXT_CMP,
expr.span,
"you should use the `starts_with` method",
lint,
info.expr.span,
&format!("you should use the `{}` method", suggest),
"like this",
format!("{}{}.starts_with({})",
if eq { "" } else { "!" },
format!("{}{}.{}({})",
if info.eq { "" } else { "!" },
snippet(cx, args[0][0].span, "_"),
suggest,
snippet(cx, arg_char[0].span, "_")));

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

/// Checks for the `CHARS_NEXT_CMP` lint.
fn lint_chars_next_cmp<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo) -> bool {
lint_chars_cmp(cx, info, &["chars", "next"], CHARS_NEXT_CMP, "starts_with")
}

/// Checks for the `CHARS_LAST_CMP` lint.
fn lint_chars_last_cmp<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo) -> bool {
if lint_chars_cmp(cx, info, &["chars", "last"], CHARS_NEXT_CMP, "ends_with") {
true
} else {
lint_chars_cmp(cx, info, &["chars", "next_back"], CHARS_NEXT_CMP, "ends_with")
}
}

/// Wrapper fn for `CHARS_NEXT_CMP` and `CHARS_LAST_CMP` lints with `unwrap()`.
fn lint_chars_cmp_with_unwrap<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo, chain_methods: &[&str], lint: &'static Lint, suggest: &str) -> bool {
if_let_chain! {[
let Some(args) = method_chain_args(info.chain, chain_methods),
let hir::ExprLit(ref lit) = info.other.node,
let ast::LitKind::Char(c) = lit.node,
], {
span_lint_and_sugg(
cx,
lint,
info.expr.span,
&format!("you should use the `{}` method", suggest),
"like this",
format!("{}{}.{}('{}')",
if info.eq { "" } else { "!" },
snippet(cx, args[0][0].span, "_"),
suggest,
c)
);

return true;
}}

false
}

/// Checks for the `CHARS_NEXT_CMP` lint with `unwrap()`.
fn lint_chars_next_cmp_with_unwrap<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo) -> bool {
lint_chars_cmp_with_unwrap(cx, info, &["chars", "next", "unwrap"], CHARS_NEXT_CMP, "starts_with")
}

/// Checks for the `CHARS_LAST_CMP` lint with `unwrap()`.
fn lint_chars_last_cmp_with_unwrap<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, info: &BinaryExprInfo) -> bool {
if lint_chars_cmp_with_unwrap(cx, info, &["chars", "last", "unwrap"], CHARS_LAST_CMP, "ends_with") {
true
} else {
lint_chars_cmp_with_unwrap(cx, info, &["chars", "next_back", "unwrap"], CHARS_LAST_CMP, "ends_with")
}
}

/// lint for length-1 `str`s for methods in `PATTERN_METHODS`
fn lint_single_char_pattern<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &'tcx hir::Expr, arg: &'tcx hir::Expr) {
let parent_item = cx.tcx.hir.get_parent(arg.id);
Expand Down
30 changes: 30 additions & 0 deletions tests/ui/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,3 +547,33 @@ fn iter_clone_collect() {
let v3 : HashSet<isize> = v.iter().cloned().collect();
let v4 : VecDeque<isize> = v.iter().cloned().collect();
}

fn chars_cmp_with_unwrap() {
let s = String::from("foo");
if s.chars().next().unwrap() == 'f' { // s.starts_with('f')
// Nothing here
}
if s.chars().next_back().unwrap() == 'o' { // s.ends_with('o')
// Nothing here
}
if s.chars().last().unwrap() == 'o' { // s.ends_with('o')
// Nothing here
}
if s.chars().next().unwrap() != 'f' { // !s.starts_with('f')
// Nothing here
}
if s.chars().next_back().unwrap() != 'o' { // !s.ends_with('o')
// Nothing here
}
if s.chars().last().unwrap() != 'o' { // !s.ends_with('o')
// Nothing here
}
}

#[allow(unnecessary_operation)]
fn ends_with() {
"".chars().last() == Some(' ');
Some(' ') != "".chars().last();
"".chars().next_back() == Some(' ');
Some(' ') != "".chars().next_back();
}
100 changes: 99 additions & 1 deletion tests/ui/methods.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -738,5 +738,103 @@ error: called `cloned().collect()` on a slice to create a `Vec`. Calling `to_vec
|
= note: `-D iter-cloned-collect` implied by `-D warnings`

error: aborting due to 107 previous errors
error: you should use the `starts_with` method
--> $DIR/methods.rs:553:8
|
553 | if s.chars().next().unwrap() == 'f' { // s.starts_with('f')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `s.starts_with('f')`

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
--> $DIR/methods.rs:553:8
|
553 | if s.chars().next().unwrap() == 'f' { // s.starts_with('f')
| ^^^^^^^^^^^^^^^^^^^^^^^^^

error: you should use the `ends_with` method
--> $DIR/methods.rs:556:8
|
556 | if s.chars().next_back().unwrap() == 'o' { // s.ends_with('o')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `s.ends_with('o')`
|
= note: `-D chars-last-cmp` implied by `-D warnings`

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
--> $DIR/methods.rs:556:8
|
556 | if s.chars().next_back().unwrap() == 'o' { // s.ends_with('o')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: you should use the `ends_with` method
--> $DIR/methods.rs:559:8
|
559 | if s.chars().last().unwrap() == 'o' { // s.ends_with('o')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `s.ends_with('o')`

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
--> $DIR/methods.rs:559:8
|
559 | if s.chars().last().unwrap() == 'o' { // s.ends_with('o')
| ^^^^^^^^^^^^^^^^^^^^^^^^^

error: you should use the `starts_with` method
--> $DIR/methods.rs:562:8
|
562 | if s.chars().next().unwrap() != 'f' { // !s.starts_with('f')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!s.starts_with('f')`

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
--> $DIR/methods.rs:562:8
|
562 | if s.chars().next().unwrap() != 'f' { // !s.starts_with('f')
| ^^^^^^^^^^^^^^^^^^^^^^^^^

error: you should use the `ends_with` method
--> $DIR/methods.rs:565:8
|
565 | if s.chars().next_back().unwrap() != 'o' { // !s.ends_with('o')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!s.ends_with('o')`

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
--> $DIR/methods.rs:565:8
|
565 | if s.chars().next_back().unwrap() != 'o' { // !s.ends_with('o')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: you should use the `ends_with` method
--> $DIR/methods.rs:568:8
|
568 | if s.chars().last().unwrap() != 'o' { // !s.ends_with('o')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!s.ends_with('o')`

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
--> $DIR/methods.rs:568:8
|
568 | if s.chars().last().unwrap() != 'o' { // !s.ends_with('o')
| ^^^^^^^^^^^^^^^^^^^^^^^^^

error: you should use the `ends_with` method
--> $DIR/methods.rs:575:5
|
575 | "".chars().last() == Some(' ');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `"".ends_with(' ')`

error: you should use the `ends_with` method
--> $DIR/methods.rs:576:5
|
576 | Some(' ') != "".chars().last();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!"".ends_with(' ')`

error: you should use the `ends_with` method
--> $DIR/methods.rs:577:5
|
577 | "".chars().next_back() == Some(' ');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `"".ends_with(' ')`

error: you should use the `ends_with` method
--> $DIR/methods.rs:578:5
|
578 | Some(' ') != "".chars().next_back();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: like this: `!"".ends_with(' ')`

error: aborting due to 123 previous errors