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

new lint: unnecessary_indexing #14058

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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 @@ -6160,6 +6160,7 @@ Released 2018-09-13
[`unnecessary_first_then_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_first_then_check
[`unnecessary_fold`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_fold
[`unnecessary_get_then_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_get_then_check
[`unnecessary_indexing`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_indexing
[`unnecessary_join`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_join
[`unnecessary_lazy_evaluations`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
[`unnecessary_literal_bound`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_literal_bound
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::unit_types::UNIT_ARG_INFO,
crate::unit_types::UNIT_CMP_INFO,
crate::unnecessary_box_returns::UNNECESSARY_BOX_RETURNS_INFO,
crate::unnecessary_indexing::UNNECESSARY_INDEXING_INFO,
crate::unnecessary_literal_bound::UNNECESSARY_LITERAL_BOUND_INFO,
crate::unnecessary_map_on_constructor::UNNECESSARY_MAP_ON_CONSTRUCTOR_INFO,
crate::unnecessary_owned_empty_strings::UNNECESSARY_OWNED_EMPTY_STRINGS_INFO,
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 @@ -368,6 +368,7 @@ mod uninit_vec;
mod unit_return_expecting_ord;
mod unit_types;
mod unnecessary_box_returns;
mod unnecessary_indexing;
mod unnecessary_literal_bound;
mod unnecessary_map_on_constructor;
mod unnecessary_owned_empty_strings;
Expand Down Expand Up @@ -974,5 +975,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf)));
store.register_late_pass(|_| Box::new(unneeded_struct_pattern::UnneededStructPattern));
store.register_late_pass(|_| Box::new(unnecessary_semicolon::UnnecessarySemicolon));
store.register_late_pass(|_| Box::new(unnecessary_indexing::UnnecessaryIndexing));
// add lints here, do not remove this comment, it's used in `new_lint`
}
4 changes: 2 additions & 2 deletions clippy_lints/src/manual_strip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl<'tcx> LateLintPass<'tcx> for ManualStrip {
}

let strippings = find_stripping(cx, strip_kind, target_res, pattern, then);
if !strippings.is_empty() {
if let Some(first_stripping) = strippings.first() {
let kind_word = match strip_kind {
StripKind::Prefix => "prefix",
StripKind::Suffix => "suffix",
Expand All @@ -106,7 +106,7 @@ impl<'tcx> LateLintPass<'tcx> for ManualStrip {
span_lint_and_then(
cx,
MANUAL_STRIP,
strippings[0],
*first_stripping,
format!("stripping a {kind_word} manually"),
|diag| {
diag.span_note(test_span, format!("the {kind_word} was tested here"));
Expand Down
190 changes: 190 additions & 0 deletions clippy_lints/src/unnecessary_indexing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use std::ops::ControlFlow;

use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::snippet;
use clippy_utils::ty::is_type_diagnostic_item;
use clippy_utils::visitors::for_each_expr;
use clippy_utils::{path_to_local, path_to_local_id};
use rustc_ast::{LitKind, Mutability};
use rustc_errors::Applicability;
use rustc_hir::{Block, Expr, ExprKind, HirId, LetStmt, Node, UnOp};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::adjustment::{Adjust, AutoBorrow, AutoBorrowMutability};
use rustc_session::declare_lint_pass;
use rustc_span::{Span, sym};

declare_clippy_lint! {
/// ### What it does
/// Checks for the use of `seq.is_empty()` in an if-conditional where `seq` is a slice, array, or Vec,
/// and in which the first element of the sequence is indexed.
///
/// ### Why is this bad?
/// This code is unnecessarily complicated and can instead be simplified to the use of an
/// if..let expression which accesses the first element of the sequence.
///
/// ### Example
/// ```no_run
/// let a: &[i32] = &[1];
/// if !a.is_empty() {
/// let b = a[0];
/// }
/// ```
/// Use instead:
/// ```no_run
/// let a: &[i32] = &[1];
/// if let Some(b) = a.first() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that you can generate even simplier expressions:

  • If a is a slice ref, use: if let [b, ..] = a
  • If a is an array, use: if let [b, ..] = &a
  • If a derefs to a slice, use: if let [b, ..] = &a[..]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add it as an alternate suggestion, which one is more preferable though?

///
/// }
/// ```
#[clippy::version = "1.86.0"]
pub UNNECESSARY_INDEXING,
complexity,
"unnecessary use of `seq.is_empty()` in a conditional when if..let is more appropriate"
}

declare_lint_pass!(UnnecessaryIndexing => [UNNECESSARY_INDEXING]);

impl<'tcx> LateLintPass<'tcx> for UnnecessaryIndexing {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'_ Expr<'tcx>) {
if let Some(if_expr) = clippy_utils::higher::If::hir(expr)
&& !if_expr.cond.span.from_expansion()
// check for negation
&& let ExprKind::Unary(UnOp::Not, unary_inner) = if_expr.cond.kind
// check for call of is_empty
&& let ExprKind::MethodCall(method, conditional_receiver, _, _) = unary_inner.kind
&& method.ident.as_str() == "is_empty"
&& let expr_ty = cx.typeck_results().expr_ty(conditional_receiver)
&& let peeled = expr_ty.peel_refs()
&& (peeled.is_slice() || peeled.is_array() || is_type_diagnostic_item(cx, peeled, sym::Vec))
&& let ExprKind::Block(block, _) = if_expr.then.kind
// do not lint if conditional receiver is mutable reference
&& expr_ty.ref_mutability() != Some(Mutability::Mut)
&& let Some(con_path) = path_to_local(conditional_receiver)
&& let Some(r) = process_indexing(cx, block, con_path)
&& let Some(receiver_span) = r.index_receiver_span
{
let receiver = snippet(cx, receiver_span, "..");
let mut suggestions: Vec<(Span, String)> = vec![];
let mut message = "consider using `if..let` syntax instead of indexing".to_string();
span_lint_and_then(
cx,
UNNECESSARY_INDEXING,
expr.span,
"condition can be simplified with `if..let` syntax",
|diag| {
if let Some(first_local) = r.first_local
&& let Some(name) = first_local.pat.simple_ident().map(|ident| ident.name)
{
suggestions.push((
if_expr.cond.span,
format!(
"let Some({}{name}) = {receiver}.first()",
// if we arent borrowing anything then we can pass a reference here for correctness
if r.extra_exprs_borrow.is_empty() { "&" } else { "" },
),
));
suggestions.push((first_local.span, String::new()));

if !r.extra_exprs_borrow.is_empty() {
suggestions.extend(
r.extra_exprs_borrow
.iter()
.chain(r.extra_exprs_copy.iter())
.map(|x| (x.span, name.to_string())),
);

message.push_str(", and replacing indexing expression(s) with the value in `Some` variant");
} else if !r.extra_exprs_copy.is_empty() {
suggestions.extend(r.extra_exprs_copy.iter().map(|x| (x.span, name.to_string())));
}
} else if r.extra_exprs_borrow.is_empty() {
suggestions.push((if_expr.cond.span, format!("let Some(&element) = {receiver}.first()")));
suggestions.extend(r.extra_exprs_copy.iter().map(|x| (x.span, "element".to_owned())));
} else {
suggestions.push((if_expr.cond.span, format!("let Some(element) = {receiver}.first()")));
suggestions.extend(
r.extra_exprs_borrow
.iter()
.chain(r.extra_exprs_copy.iter())
.map(|x| (x.span, "element".to_owned())),
);
}

diag.multipart_suggestion(message, suggestions, Applicability::MaybeIncorrect);
},
);
}
}
}

struct IndexCheckResult<'a> {
// span of the receiver for the index operation, only Some in the event the indexing is via a direct primitive
index_receiver_span: Option<Span>,
// first local in the block - used as pattern for `Some(pat)`
first_local: Option<&'a LetStmt<'a>>,
// any other index expressions to replace with `pat` (or "element" if no local exists)
extra_exprs_borrow: Vec<&'a Expr<'a>>,
// copied extra index expressions: if we only have these and no borrows we can provide a correct suggestion of `let
// Some(&a) = ...`
extra_exprs_copy: Vec<&'a Expr<'a>>,
}

/// Checks the block for any indexing of the conditional receiver. Returns `None` if the block
/// contains code that invalidates the lint, e.g., the receiver is accessed via a mutable reference.
fn process_indexing<'a>(
cx: &LateContext<'a>,
block: &'a Block<'a>,
conditional_receiver_hid: HirId,
) -> Option<IndexCheckResult<'a>> {
let mut index_receiver_span: Option<Span> = None;
let mut first_local: Option<&LetStmt<'_>> = None;
let mut extra_exprs_borrow: Vec<&Expr<'_>> = vec![];
let mut extra_exprs_copy: Vec<&Expr<'_>> = vec![];

// if res == Some(()), then mutation occurred
// & therefore we should not lint on this
let res = for_each_expr(cx, block, |x| {
let adjustments = cx.typeck_results().expr_adjustments(x);
if let ExprKind::Index(receiver, index, _) = x.kind
&& let ExprKind::Lit(lit) = index.kind
&& let LitKind::Int(val, _) = lit.node
&& path_to_local_id(receiver, conditional_receiver_hid)
&& val.0 == 0
{
index_receiver_span = Some(receiver.span);
if let Node::LetStmt(local) = cx.tcx.parent_hir_node(x.hir_id) {
if first_local.is_none() {
first_local = Some(local);
return ControlFlow::Continue::<()>(());
}
}

if let Node::Expr(x) = cx.tcx.parent_hir_node(x.hir_id)
&& let ExprKind::AddrOf(_, _, _) = x.kind
{
extra_exprs_borrow.push(x);
} else {
extra_exprs_copy.push(x);
}
} else if adjustments.iter().any(|adjustment| {
matches!(
adjustment.kind,
Adjust::Borrow(AutoBorrow::Ref(AutoBorrowMutability::Mut { .. }))
)
}) {
// do not lint on mutable auto borrows (https://github.com/rust-lang/rust-clippy/pull/12464#discussion_r1600352696)
return ControlFlow::Break(());
} else if let ExprKind::AddrOf(_, Mutability::Mut, _) = x.kind {
return ControlFlow::Break(());
}

ControlFlow::Continue::<()>(())
});

res.is_none().then_some(IndexCheckResult {
index_receiver_span,
first_local,
extra_exprs_borrow,
extra_exprs_copy,
})
}
1 change: 1 addition & 0 deletions tests/ui/index_refutable_slice/if_let_slice_binding.fixed
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![deny(clippy::index_refutable_slice)]
#![allow(clippy::uninlined_format_args, clippy::needless_lifetimes)]
#![allow(clippy::unnecessary_indexing)]

enum SomeEnum<T> {
One(T),
Expand Down
1 change: 1 addition & 0 deletions tests/ui/index_refutable_slice/if_let_slice_binding.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![deny(clippy::index_refutable_slice)]
#![allow(clippy::uninlined_format_args, clippy::needless_lifetimes)]
#![allow(clippy::unnecessary_indexing)]

enum SomeEnum<T> {
One(T),
Expand Down
20 changes: 10 additions & 10 deletions tests/ui/index_refutable_slice/if_let_slice_binding.stderr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:14:17
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:15:17
|
LL | if let Some(slice) = slice {
| ^^^^^
Expand All @@ -17,7 +17,7 @@ LL ~ println!("{}", slice_0);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:21:17
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:22:17
|
LL | if let Some(slice) = slice {
| ^^^^^
Expand All @@ -30,7 +30,7 @@ LL ~ println!("{}", slice_0);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:28:17
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:29:17
|
LL | if let Some(slice) = slice {
| ^^^^^
Expand All @@ -44,7 +44,7 @@ LL ~ println!("{}", slice_0);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:36:26
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:37:26
|
LL | if let SomeEnum::One(slice) | SomeEnum::Three(slice) = slice_wrapped {
| ^^^^^
Expand All @@ -57,7 +57,7 @@ LL ~ println!("{}", slice_0);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:44:29
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:45:29
|
LL | if let (SomeEnum::Three(a), Some(b)) = (a_wrapped, b_wrapped) {
| ^
Expand All @@ -71,7 +71,7 @@ LL ~ println!("{} -> {}", a_2, b[1]);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:44:38
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:45:38
|
LL | if let (SomeEnum::Three(a), Some(b)) = (a_wrapped, b_wrapped) {
| ^
Expand All @@ -85,7 +85,7 @@ LL ~ println!("{} -> {}", a[2], b_1);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:53:21
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:54:21
|
LL | if let Some(ref slice) = slice {
| ^^^^^
Expand All @@ -98,7 +98,7 @@ LL ~ println!("{:?}", slice_1);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:62:17
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:63:17
|
LL | if let Some(slice) = &slice {
| ^^^^^
Expand All @@ -111,7 +111,7 @@ LL ~ println!("{:?}", slice_0);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:132:17
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:133:17
|
LL | if let Some(slice) = wrap.inner {
| ^^^^^
Expand All @@ -125,7 +125,7 @@ LL ~ println!("This is awesome! {}", slice_0);
|

error: this binding can be a slice pattern to avoid indexing
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:140:17
--> tests/ui/index_refutable_slice/if_let_slice_binding.rs:141:17
|
LL | if let Some(slice) = wrap.inner {
| ^^^^^
Expand Down
Loading
Loading