rust/clippy_lints/src/loops/utils.rs
2022-07-11 23:20:37 +02:00

359 lines
13 KiB
Rust

use clippy_utils::ty::{has_iter_method, implements_trait};
use clippy_utils::{get_parent_expr, is_integer_const, path_to_local, path_to_local_id, sugg};
use if_chain::if_chain;
use rustc_ast::ast::{LitIntType, LitKind};
use rustc_errors::Applicability;
use rustc_hir::intravisit::{walk_expr, walk_local, walk_pat, walk_stmt, Visitor};
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, HirId, HirIdMap, Local, Mutability, Pat, PatKind, Stmt};
use rustc_lint::LateContext;
use rustc_middle::hir::nested_filter;
use rustc_middle::ty::{self, Ty};
use rustc_span::source_map::Spanned;
use rustc_span::symbol::{sym, Symbol};
use rustc_typeck::hir_ty_to_ty;
use std::iter::Iterator;
#[derive(Debug, PartialEq, Eq)]
enum IncrementVisitorVarState {
Initial, // Not examined yet
IncrOnce, // Incremented exactly once, may be a loop counter
DontWarn,
}
/// Scan a for loop for variables that are incremented exactly once and not used after that.
pub(super) struct IncrementVisitor<'a, 'tcx> {
cx: &'a LateContext<'tcx>, // context reference
states: HirIdMap<IncrementVisitorVarState>, // incremented variables
depth: u32, // depth of conditional expressions
done: bool,
}
impl<'a, 'tcx> IncrementVisitor<'a, 'tcx> {
pub(super) fn new(cx: &'a LateContext<'tcx>) -> Self {
Self {
cx,
states: HirIdMap::default(),
depth: 0,
done: false,
}
}
pub(super) fn into_results(self) -> impl Iterator<Item = HirId> {
self.states.into_iter().filter_map(|(id, state)| {
if state == IncrementVisitorVarState::IncrOnce {
Some(id)
} else {
None
}
})
}
}
impl<'a, 'tcx> Visitor<'tcx> for IncrementVisitor<'a, 'tcx> {
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
if self.done {
return;
}
// If node is a variable
if let Some(def_id) = path_to_local(expr) {
if let Some(parent) = get_parent_expr(self.cx, expr) {
let state = self.states.entry(def_id).or_insert(IncrementVisitorVarState::Initial);
if *state == IncrementVisitorVarState::IncrOnce {
*state = IncrementVisitorVarState::DontWarn;
return;
}
match parent.kind {
ExprKind::AssignOp(op, lhs, rhs) => {
if lhs.hir_id == expr.hir_id {
*state = if op.node == BinOpKind::Add
&& is_integer_const(self.cx, rhs, 1)
&& *state == IncrementVisitorVarState::Initial
&& self.depth == 0
{
IncrementVisitorVarState::IncrOnce
} else {
// Assigned some other value or assigned multiple times
IncrementVisitorVarState::DontWarn
};
}
},
ExprKind::Assign(lhs, _, _) if lhs.hir_id == expr.hir_id => {
*state = IncrementVisitorVarState::DontWarn;
},
ExprKind::AddrOf(BorrowKind::Ref, mutability, _) if mutability == Mutability::Mut => {
*state = IncrementVisitorVarState::DontWarn;
},
_ => (),
}
}
walk_expr(self, expr);
} else if is_loop(expr) || is_conditional(expr) {
self.depth += 1;
walk_expr(self, expr);
self.depth -= 1;
} else if let ExprKind::Continue(_) = expr.kind {
self.done = true;
} else {
walk_expr(self, expr);
}
}
}
enum InitializeVisitorState<'hir> {
Initial, // Not examined yet
Declared(Symbol, Option<Ty<'hir>>), // Declared but not (yet) initialized
Initialized {
name: Symbol,
ty: Option<Ty<'hir>>,
initializer: &'hir Expr<'hir>,
},
DontWarn,
}
/// Checks whether a variable is initialized at the start of a loop and not modified
/// and used after the loop.
pub(super) struct InitializeVisitor<'a, 'tcx> {
cx: &'a LateContext<'tcx>, // context reference
end_expr: &'tcx Expr<'tcx>, // the for loop. Stop scanning here.
var_id: HirId,
state: InitializeVisitorState<'tcx>,
depth: u32, // depth of conditional expressions
past_loop: bool,
}
impl<'a, 'tcx> InitializeVisitor<'a, 'tcx> {
pub(super) fn new(cx: &'a LateContext<'tcx>, end_expr: &'tcx Expr<'tcx>, var_id: HirId) -> Self {
Self {
cx,
end_expr,
var_id,
state: InitializeVisitorState::Initial,
depth: 0,
past_loop: false,
}
}
pub(super) fn get_result(&self) -> Option<(Symbol, Option<Ty<'tcx>>, &'tcx Expr<'tcx>)> {
if let InitializeVisitorState::Initialized { name, ty, initializer } = self.state {
Some((name, ty, initializer))
} else {
None
}
}
}
impl<'a, 'tcx> Visitor<'tcx> for InitializeVisitor<'a, 'tcx> {
type NestedFilter = nested_filter::OnlyBodies;
fn visit_local(&mut self, l: &'tcx Local<'_>) {
// Look for declarations of the variable
if_chain! {
if l.pat.hir_id == self.var_id;
if let PatKind::Binding(.., ident, _) = l.pat.kind;
then {
let ty = l.ty.map(|ty| hir_ty_to_ty(self.cx.tcx, ty));
self.state = l.init.map_or(InitializeVisitorState::Declared(ident.name, ty), |init| {
InitializeVisitorState::Initialized {
initializer: init,
ty,
name: ident.name,
}
})
}
}
walk_local(self, l);
}
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
if matches!(self.state, InitializeVisitorState::DontWarn) {
return;
}
if expr.hir_id == self.end_expr.hir_id {
self.past_loop = true;
return;
}
// No need to visit expressions before the variable is
// declared
if matches!(self.state, InitializeVisitorState::Initial) {
return;
}
// If node is the desired variable, see how it's used
if path_to_local_id(expr, self.var_id) {
if self.past_loop {
self.state = InitializeVisitorState::DontWarn;
return;
}
if let Some(parent) = get_parent_expr(self.cx, expr) {
match parent.kind {
ExprKind::AssignOp(_, lhs, _) if lhs.hir_id == expr.hir_id => {
self.state = InitializeVisitorState::DontWarn;
},
ExprKind::Assign(lhs, rhs, _) if lhs.hir_id == expr.hir_id => {
self.state = if self.depth == 0 {
match self.state {
InitializeVisitorState::Declared(name, mut ty) => {
if ty.is_none() {
if let ExprKind::Lit(Spanned {
node: LitKind::Int(_, LitIntType::Unsuffixed),
..
}) = rhs.kind
{
ty = None;
} else {
ty = self.cx.typeck_results().expr_ty_opt(rhs);
}
}
InitializeVisitorState::Initialized {
initializer: rhs,
ty,
name,
}
},
InitializeVisitorState::Initialized { ty, name, .. } => {
InitializeVisitorState::Initialized {
initializer: rhs,
ty,
name,
}
},
_ => InitializeVisitorState::DontWarn,
}
} else {
InitializeVisitorState::DontWarn
}
},
ExprKind::AddrOf(BorrowKind::Ref, mutability, _) if mutability == Mutability::Mut => {
self.state = InitializeVisitorState::DontWarn;
},
_ => (),
}
}
walk_expr(self, expr);
} else if !self.past_loop && is_loop(expr) {
self.state = InitializeVisitorState::DontWarn;
} else if is_conditional(expr) {
self.depth += 1;
walk_expr(self, expr);
self.depth -= 1;
} else {
walk_expr(self, expr);
}
}
fn nested_visit_map(&mut self) -> Self::Map {
self.cx.tcx.hir()
}
}
fn is_loop(expr: &Expr<'_>) -> bool {
matches!(expr.kind, ExprKind::Loop(..))
}
fn is_conditional(expr: &Expr<'_>) -> bool {
matches!(expr.kind, ExprKind::If(..) | ExprKind::Match(..))
}
#[derive(PartialEq, Eq)]
pub(super) enum Nesting {
Unknown, // no nesting detected yet
RuledOut, // the iterator is initialized or assigned within scope
LookFurther, // no nesting detected, no further walk required
}
use self::Nesting::{LookFurther, RuledOut, Unknown};
pub(super) struct LoopNestVisitor {
pub(super) hir_id: HirId,
pub(super) iterator: HirId,
pub(super) nesting: Nesting,
}
impl<'tcx> Visitor<'tcx> for LoopNestVisitor {
fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) {
if stmt.hir_id == self.hir_id {
self.nesting = LookFurther;
} else if self.nesting == Unknown {
walk_stmt(self, stmt);
}
}
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
if self.nesting != Unknown {
return;
}
if expr.hir_id == self.hir_id {
self.nesting = LookFurther;
return;
}
match expr.kind {
ExprKind::Assign(path, _, _) | ExprKind::AssignOp(_, path, _) => {
if path_to_local_id(path, self.iterator) {
self.nesting = RuledOut;
}
},
_ => walk_expr(self, expr),
}
}
fn visit_pat(&mut self, pat: &'tcx Pat<'_>) {
if self.nesting != Unknown {
return;
}
if let PatKind::Binding(_, id, ..) = pat.kind {
if id == self.iterator {
self.nesting = RuledOut;
return;
}
}
walk_pat(self, pat);
}
}
/// If `arg` was the argument to a `for` loop, return the "cleanest" way of writing the
/// actual `Iterator` that the loop uses.
pub(super) fn make_iterator_snippet(cx: &LateContext<'_>, arg: &Expr<'_>, applic_ref: &mut Applicability) -> String {
let impls_iterator = cx.tcx.get_diagnostic_item(sym::Iterator).map_or(false, |id| {
implements_trait(cx, cx.typeck_results().expr_ty(arg), id, &[])
});
if impls_iterator {
format!(
"{}",
sugg::Sugg::hir_with_applicability(cx, arg, "_", applic_ref).maybe_par()
)
} else {
// (&x).into_iter() ==> x.iter()
// (&mut x).into_iter() ==> x.iter_mut()
let arg_ty = cx.typeck_results().expr_ty_adjusted(arg);
match &arg_ty.kind() {
ty::Ref(_, inner_ty, mutbl) if has_iter_method(cx, *inner_ty).is_some() => {
let method_name = match mutbl {
Mutability::Mut => "iter_mut",
Mutability::Not => "iter",
};
let caller = match &arg.kind {
ExprKind::AddrOf(BorrowKind::Ref, _, arg_inner) => arg_inner,
_ => arg,
};
format!(
"{}.{}()",
sugg::Sugg::hir_with_applicability(cx, caller, "_", applic_ref).maybe_par(),
method_name,
)
},
_ => format!(
"{}.into_iter()",
sugg::Sugg::hir_with_applicability(cx, arg, "_", applic_ref).maybe_par()
),
}
}
}