clippy_utils/
eager_or_lazy.rs

1//! Utilities for evaluating whether eagerly evaluated expressions can be made lazy and vice versa.
2//!
3//! Things to consider:
4//!  - does the expression have side-effects?
5//!  - is the expression computationally expensive?
6//!
7//! See lints:
8//!  - unnecessary-lazy-evaluations
9//!  - or-fun-call
10//!  - option-if-let-else
11
12use crate::consts::{ConstEvalCtxt, FullInt};
13use crate::ty::{all_predicates_of, is_copy};
14use crate::visitors::is_const_evaluatable;
15use rustc_hir::def::{DefKind, Res};
16use rustc_hir::def_id::DefId;
17use rustc_hir::intravisit::{Visitor, walk_expr};
18use rustc_hir::{BinOpKind, Block, Expr, ExprKind, QPath, UnOp};
19use rustc_lint::LateContext;
20use rustc_middle::ty;
21use rustc_middle::ty::adjustment::Adjust;
22use rustc_span::{Symbol, sym};
23use std::{cmp, ops};
24
25#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26enum EagernessSuggestion {
27    // The expression is cheap and should be evaluated eagerly
28    Eager,
29    // The expression may be cheap, so don't suggested lazy evaluation; or the expression may not be safe to switch to
30    // eager evaluation.
31    NoChange,
32    // The expression is likely expensive and should be evaluated lazily.
33    Lazy,
34    // The expression cannot be placed into a closure.
35    ForceNoChange,
36}
37impl ops::BitOr for EagernessSuggestion {
38    type Output = Self;
39    fn bitor(self, rhs: Self) -> Self {
40        cmp::max(self, rhs)
41    }
42}
43impl ops::BitOrAssign for EagernessSuggestion {
44    fn bitor_assign(&mut self, rhs: Self) {
45        *self = *self | rhs;
46    }
47}
48
49/// Determine the eagerness of the given function call.
50fn fn_eagerness(cx: &LateContext<'_>, fn_id: DefId, name: Symbol, have_one_arg: bool) -> EagernessSuggestion {
51    use EagernessSuggestion::{Eager, Lazy, NoChange};
52    let name = name.as_str();
53
54    let ty = match cx.tcx.impl_of_method(fn_id) {
55        Some(id) => cx.tcx.type_of(id).instantiate_identity(),
56        None => return Lazy,
57    };
58
59    if (name.starts_with("as_") || name == "len" || name == "is_empty") && have_one_arg {
60        if matches!(
61            cx.tcx.crate_name(fn_id.krate),
62            sym::std | sym::core | sym::alloc | sym::proc_macro
63        ) {
64            Eager
65        } else {
66            NoChange
67        }
68    } else if let ty::Adt(def, subs) = ty.kind() {
69        // Types where the only fields are generic types (or references to) with no trait bounds other
70        // than marker traits.
71        // Due to the limited operations on these types functions should be fairly cheap.
72        if def.variants().iter().flat_map(|v| v.fields.iter()).any(|x| {
73            matches!(
74                cx.tcx.type_of(x.did).instantiate_identity().peel_refs().kind(),
75                ty::Param(_)
76            )
77        }) && all_predicates_of(cx.tcx, fn_id).all(|(pred, _)| match pred.kind().skip_binder() {
78            ty::ClauseKind::Trait(pred) => cx.tcx.trait_def(pred.trait_ref.def_id).is_marker,
79            _ => true,
80        }) && subs.types().all(|x| matches!(x.peel_refs().kind(), ty::Param(_)))
81        {
82            // Limit the function to either `(self) -> bool` or `(&self) -> bool`
83            match &**cx
84                .tcx
85                .fn_sig(fn_id)
86                .instantiate_identity()
87                .skip_binder()
88                .inputs_and_output
89            {
90                [arg, res] if !arg.is_mutable_ptr() && arg.peel_refs() == ty && res.is_bool() => NoChange,
91                _ => Lazy,
92            }
93        } else {
94            Lazy
95        }
96    } else {
97        Lazy
98    }
99}
100
101fn res_has_significant_drop(res: Res, cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
102    if let Res::Def(DefKind::Ctor(..) | DefKind::Variant | DefKind::Enum | DefKind::Struct, _)
103    | Res::SelfCtor(_)
104    | Res::SelfTyAlias { .. } = res
105    {
106        cx.typeck_results()
107            .expr_ty(e)
108            .has_significant_drop(cx.tcx, cx.typing_env())
109    } else {
110        false
111    }
112}
113
114#[expect(clippy::too_many_lines)]
115fn expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessSuggestion {
116    struct V<'cx, 'tcx> {
117        cx: &'cx LateContext<'tcx>,
118        eagerness: EagernessSuggestion,
119    }
120
121    impl<'tcx> Visitor<'tcx> for V<'_, 'tcx> {
122        fn visit_expr(&mut self, e: &'tcx Expr<'_>) {
123            use EagernessSuggestion::{ForceNoChange, Lazy, NoChange};
124            if self.eagerness == ForceNoChange {
125                return;
126            }
127
128            // Autoderef through a user-defined `Deref` impl can have side-effects,
129            // so don't suggest changing it.
130            if self
131                .cx
132                .typeck_results()
133                .expr_adjustments(e)
134                .iter()
135                .any(|adj| matches!(adj.kind, Adjust::Deref(Some(_))))
136            {
137                self.eagerness |= NoChange;
138                return;
139            }
140
141            match e.kind {
142                ExprKind::Call(
143                    &Expr {
144                        kind: ExprKind::Path(ref path),
145                        hir_id,
146                        ..
147                    },
148                    args,
149                ) => match self.cx.qpath_res(path, hir_id) {
150                    res @ (Res::Def(DefKind::Ctor(..) | DefKind::Variant, _) | Res::SelfCtor(_)) => {
151                        if res_has_significant_drop(res, self.cx, e) {
152                            self.eagerness = ForceNoChange;
153                            return;
154                        }
155                    },
156                    Res::Def(_, id) if self.cx.tcx.is_promotable_const_fn(id) => (),
157                    // No need to walk the arguments here, `is_const_evaluatable` already did
158                    Res::Def(..) if is_const_evaluatable(self.cx, e) => {
159                        self.eagerness |= NoChange;
160                        return;
161                    },
162                    Res::Def(_, id) => match path {
163                        QPath::Resolved(_, p) => {
164                            self.eagerness |=
165                                fn_eagerness(self.cx, id, p.segments.last().unwrap().ident.name, !args.is_empty());
166                        },
167                        QPath::TypeRelative(_, name) => {
168                            self.eagerness |= fn_eagerness(self.cx, id, name.ident.name, !args.is_empty());
169                        },
170                        QPath::LangItem(..) => self.eagerness = Lazy,
171                    },
172                    _ => self.eagerness = Lazy,
173                },
174                // No need to walk the arguments here, `is_const_evaluatable` already did
175                ExprKind::MethodCall(..) if is_const_evaluatable(self.cx, e) => {
176                    self.eagerness |= NoChange;
177                    return;
178                },
179                #[expect(clippy::match_same_arms)] // arm pattern can't be merged due to `ref`, see rust#105778
180                ExprKind::Struct(path, ..) => {
181                    if res_has_significant_drop(self.cx.qpath_res(path, e.hir_id), self.cx, e) {
182                        self.eagerness = ForceNoChange;
183                        return;
184                    }
185                },
186                ExprKind::Path(ref path) => {
187                    if res_has_significant_drop(self.cx.qpath_res(path, e.hir_id), self.cx, e) {
188                        self.eagerness = ForceNoChange;
189                        return;
190                    }
191                },
192                ExprKind::MethodCall(name, ..) => {
193                    self.eagerness |= self
194                        .cx
195                        .typeck_results()
196                        .type_dependent_def_id(e.hir_id)
197                        .map_or(Lazy, |id| fn_eagerness(self.cx, id, name.ident.name, true));
198                },
199                ExprKind::Index(_, e, _) => {
200                    let ty = self.cx.typeck_results().expr_ty_adjusted(e);
201                    if is_copy(self.cx, ty) && !ty.is_ref() {
202                        self.eagerness |= NoChange;
203                    } else {
204                        self.eagerness = Lazy;
205                    }
206                },
207
208                // `-i32::MIN` panics with overflow checks
209                ExprKind::Unary(UnOp::Neg, right) if ConstEvalCtxt::new(self.cx).eval(right).is_none() => {
210                    self.eagerness |= NoChange;
211                },
212
213                // Custom `Deref` impl might have side effects
214                ExprKind::Unary(UnOp::Deref, e)
215                    if self.cx.typeck_results().expr_ty(e).builtin_deref(true).is_none() =>
216                {
217                    self.eagerness |= NoChange;
218                },
219                // Dereferences should be cheap, but dereferencing a raw pointer earlier may not be safe.
220                ExprKind::Unary(UnOp::Deref, e) if !self.cx.typeck_results().expr_ty(e).is_raw_ptr() => (),
221                ExprKind::Unary(UnOp::Deref, _) => self.eagerness |= NoChange,
222                ExprKind::Unary(_, e)
223                    if matches!(
224                        self.cx.typeck_results().expr_ty(e).kind(),
225                        ty::Bool | ty::Int(_) | ty::Uint(_),
226                    ) => {},
227
228                // `>>` and `<<` panic when the right-hand side is greater than or equal to the number of bits in the
229                // type of the left-hand side, or is negative.
230                // We intentionally only check if the right-hand isn't a constant, because even if the suggestion would
231                // overflow with constants, the compiler emits an error for it and the programmer will have to fix it.
232                // Thus, we would realistically only delay the lint.
233                ExprKind::Binary(op, _, right)
234                    if matches!(op.node, BinOpKind::Shl | BinOpKind::Shr)
235                        && ConstEvalCtxt::new(self.cx).eval(right).is_none() =>
236                {
237                    self.eagerness |= NoChange;
238                },
239
240                ExprKind::Binary(op, left, right)
241                    if matches!(op.node, BinOpKind::Div | BinOpKind::Rem)
242                        && let right_ty = self.cx.typeck_results().expr_ty(right)
243                        && let ecx = ConstEvalCtxt::new(self.cx)
244                        && let left = ecx.eval(left)
245                        && let right = ecx.eval(right).and_then(|c| c.int_value(self.cx.tcx, right_ty))
246                        && matches!(
247                            (left, right),
248                            // `1 / x`: x might be zero
249                            (_, None)
250                            // `x / -1`: x might be T::MIN
251                            | (None, Some(FullInt::S(-1)))
252                        ) =>
253                {
254                    self.eagerness |= NoChange;
255                },
256
257                // Similar to `>>` and `<<`, we only want to avoid linting entirely if either side is unknown and the
258                // compiler can't emit an error for an overflowing expression.
259                // Suggesting eagerness for `true.then(|| i32::MAX + 1)` is okay because the compiler will emit an
260                // error and it's good to have the eagerness warning up front when the user fixes the logic error.
261                ExprKind::Binary(op, left, right)
262                    if matches!(op.node, BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul)
263                        && !self.cx.typeck_results().expr_ty(e).is_floating_point()
264                        && let ecx = ConstEvalCtxt::new(self.cx)
265                        && (ecx.eval(left).is_none() || ecx.eval(right).is_none()) =>
266                {
267                    self.eagerness |= NoChange;
268                },
269
270                ExprKind::Binary(_, lhs, rhs)
271                    if self.cx.typeck_results().expr_ty(lhs).is_primitive()
272                        && self.cx.typeck_results().expr_ty(rhs).is_primitive() => {},
273
274                // Can't be moved into a closure
275                ExprKind::Break(..)
276                | ExprKind::Continue(_)
277                | ExprKind::Ret(_)
278                | ExprKind::Become(_)
279                | ExprKind::InlineAsm(_)
280                | ExprKind::Yield(..)
281                | ExprKind::Err(_) => {
282                    self.eagerness = ForceNoChange;
283                    return;
284                },
285
286                // Memory allocation, custom operator, loop, or call to an unknown function
287                ExprKind::Unary(..) | ExprKind::Binary(..) | ExprKind::Loop(..) | ExprKind::Call(..) => {
288                    self.eagerness = Lazy;
289                },
290
291                ExprKind::ConstBlock(_)
292                | ExprKind::Array(_)
293                | ExprKind::Tup(_)
294                | ExprKind::Lit(_)
295                | ExprKind::Cast(..)
296                | ExprKind::Type(..)
297                | ExprKind::DropTemps(_)
298                | ExprKind::Let(..)
299                | ExprKind::If(..)
300                | ExprKind::Match(..)
301                | ExprKind::Closure { .. }
302                | ExprKind::Field(..)
303                | ExprKind::AddrOf(..)
304                | ExprKind::Repeat(..)
305                | ExprKind::Block(Block { stmts: [], .. }, _)
306                | ExprKind::OffsetOf(..)
307                | ExprKind::UnsafeBinderCast(..) => (),
308
309                // Assignment might be to a local defined earlier, so don't eagerly evaluate.
310                // Blocks with multiple statements might be expensive, so don't eagerly evaluate.
311                // TODO: Actually check if either of these are true here.
312                ExprKind::Assign(..) | ExprKind::AssignOp(..) | ExprKind::Block(..) => self.eagerness |= NoChange,
313            }
314            walk_expr(self, e);
315        }
316    }
317
318    let mut v = V {
319        cx,
320        eagerness: EagernessSuggestion::Eager,
321    };
322    v.visit_expr(e);
323    v.eagerness
324}
325
326/// Whether the given expression should be changed to evaluate eagerly
327pub fn switch_to_eager_eval<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool {
328    expr_eagerness(cx, expr) == EagernessSuggestion::Eager
329}
330
331/// Whether the given expression should be changed to evaluate lazily
332pub fn switch_to_lazy_eval<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool {
333    expr_eagerness(cx, expr) == EagernessSuggestion::Lazy
334}