rustc_lint/
for_loops_over_fallibles.rs

1use hir::{Expr, Pat};
2use rustc_hir::{self as hir, LangItem};
3use rustc_infer::infer::TyCtxtInferExt;
4use rustc_infer::traits::ObligationCause;
5use rustc_middle::ty;
6use rustc_session::{declare_lint, declare_lint_pass};
7use rustc_span::{Span, sym};
8use rustc_trait_selection::traits::ObligationCtxt;
9
10use crate::lints::{
11    ForLoopsOverFalliblesDiag, ForLoopsOverFalliblesLoopSub, ForLoopsOverFalliblesQuestionMark,
12    ForLoopsOverFalliblesSuggestion,
13};
14use crate::{LateContext, LateLintPass, LintContext};
15
16declare_lint! {
17    /// The `for_loops_over_fallibles` lint checks for `for` loops over `Option` or `Result` values.
18    ///
19    /// ### Example
20    ///
21    /// ```rust
22    /// let opt = Some(1);
23    /// for x in opt { /* ... */}
24    /// ```
25    ///
26    /// {{produces}}
27    ///
28    /// ### Explanation
29    ///
30    /// Both `Option` and `Result` implement `IntoIterator` trait, which allows using them in a `for` loop.
31    /// `for` loop over `Option` or `Result` will iterate either 0 (if the value is `None`/`Err(_)`)
32    /// or 1 time (if the value is `Some(_)`/`Ok(_)`). This is not very useful and is more clearly expressed
33    /// via `if let`.
34    ///
35    /// `for` loop can also be accidentally written with the intention to call a function multiple times,
36    /// while the function returns `Some(_)`, in these cases `while let` loop should be used instead.
37    ///
38    /// The "intended" use of `IntoIterator` implementations for `Option` and `Result` is passing them to
39    /// generic code that expects something implementing `IntoIterator`. For example using `.chain(option)`
40    /// to optionally add a value to an iterator.
41    pub FOR_LOOPS_OVER_FALLIBLES,
42    Warn,
43    "for-looping over an `Option` or a `Result`, which is more clearly expressed as an `if let`"
44}
45
46declare_lint_pass!(ForLoopsOverFallibles => [FOR_LOOPS_OVER_FALLIBLES]);
47
48impl<'tcx> LateLintPass<'tcx> for ForLoopsOverFallibles {
49    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
50        let Some((pat, arg)) = extract_for_loop(expr) else { return };
51
52        let ty = cx.typeck_results().expr_ty(arg);
53
54        let (adt, args, ref_mutability) = match ty.kind() {
55            &ty::Adt(adt, args) => (adt, args, None),
56            &ty::Ref(_, ty, mutability) => match ty.kind() {
57                &ty::Adt(adt, args) => (adt, args, Some(mutability)),
58                _ => return,
59            },
60            _ => return,
61        };
62
63        let (article, ty, var) = match adt.did() {
64            did if cx.tcx.is_diagnostic_item(sym::Option, did) && ref_mutability.is_some() => {
65                ("a", "Option", "Some")
66            }
67            did if cx.tcx.is_diagnostic_item(sym::Option, did) => ("an", "Option", "Some"),
68            did if cx.tcx.is_diagnostic_item(sym::Result, did) => ("a", "Result", "Ok"),
69            _ => return,
70        };
71
72        let ref_prefix = match ref_mutability {
73            None => "",
74            Some(ref_mutability) => ref_mutability.ref_prefix_str(),
75        };
76
77        let sub = if let Some(recv) = extract_iterator_next_call(cx, arg)
78            && let Ok(recv_snip) = cx.sess().source_map().span_to_snippet(recv.span)
79        {
80            ForLoopsOverFalliblesLoopSub::RemoveNext {
81                suggestion: recv.span.between(arg.span.shrink_to_hi()),
82                recv_snip,
83            }
84        } else {
85            ForLoopsOverFalliblesLoopSub::UseWhileLet {
86                start_span: expr.span.with_hi(pat.span.lo()),
87                end_span: pat.span.between(arg.span),
88                var,
89            }
90        };
91        let question_mark = suggest_question_mark(cx, adt, args, expr.span)
92            .then(|| ForLoopsOverFalliblesQuestionMark { suggestion: arg.span.shrink_to_hi() });
93        let suggestion = ForLoopsOverFalliblesSuggestion {
94            var,
95            start_span: expr.span.with_hi(pat.span.lo()),
96            end_span: pat.span.between(arg.span),
97        };
98
99        cx.emit_span_lint(
100            FOR_LOOPS_OVER_FALLIBLES,
101            arg.span,
102            ForLoopsOverFalliblesDiag { article, ref_prefix, ty, sub, question_mark, suggestion },
103        );
104    }
105}
106
107fn extract_for_loop<'tcx>(expr: &Expr<'tcx>) -> Option<(&'tcx Pat<'tcx>, &'tcx Expr<'tcx>)> {
108    if let hir::ExprKind::DropTemps(e) = expr.kind
109        && let hir::ExprKind::Match(iterexpr, [arm], hir::MatchSource::ForLoopDesugar) = e.kind
110        && let hir::ExprKind::Call(_, [arg]) = iterexpr.kind
111        && let hir::ExprKind::Loop(block, ..) = arm.body.kind
112        && let [stmt] = block.stmts
113        && let hir::StmtKind::Expr(e) = stmt.kind
114        && let hir::ExprKind::Match(_, [_, some_arm], _) = e.kind
115        && let hir::PatKind::Struct(_, [field], _) = some_arm.pat.kind
116    {
117        Some((field.pat, arg))
118    } else {
119        None
120    }
121}
122
123fn extract_iterator_next_call<'tcx>(
124    cx: &LateContext<'_>,
125    expr: &Expr<'tcx>,
126) -> Option<&'tcx Expr<'tcx>> {
127    // This won't work for `Iterator::next(iter)`, is this an issue?
128    if let hir::ExprKind::MethodCall(_, recv, _, _) = expr.kind
129        && cx
130            .typeck_results()
131            .type_dependent_def_id(expr.hir_id)
132            .is_some_and(|def_id| cx.tcx.is_lang_item(def_id, LangItem::IteratorNext))
133    {
134        Some(recv)
135    } else {
136        None
137    }
138}
139
140fn suggest_question_mark<'tcx>(
141    cx: &LateContext<'tcx>,
142    adt: ty::AdtDef<'tcx>,
143    args: ty::GenericArgsRef<'tcx>,
144    span: Span,
145) -> bool {
146    let Some(body_id) = cx.enclosing_body else { return false };
147    let Some(into_iterator_did) = cx.tcx.get_diagnostic_item(sym::IntoIterator) else {
148        return false;
149    };
150
151    if !cx.tcx.is_diagnostic_item(sym::Result, adt.did()) {
152        return false;
153    }
154
155    // Check that the function/closure/constant we are in has a `Result` type.
156    // Otherwise suggesting using `?` may not be a good idea.
157    {
158        let ty = cx.typeck_results().expr_ty(cx.tcx.hir_body(body_id).value);
159        let ty::Adt(ret_adt, ..) = ty.kind() else { return false };
160        if !cx.tcx.is_diagnostic_item(sym::Result, ret_adt.did()) {
161            return false;
162        }
163    }
164
165    let ty = args.type_at(0);
166    let (infcx, param_env) = cx.tcx.infer_ctxt().build_with_typing_env(cx.typing_env());
167    let ocx = ObligationCtxt::new(&infcx);
168
169    let body_def_id = cx.tcx.hir_body_owner_def_id(body_id);
170    let cause =
171        ObligationCause::new(span, body_def_id, rustc_infer::traits::ObligationCauseCode::Misc);
172
173    ocx.register_bound(
174        cause,
175        param_env,
176        // Erase any region vids from the type, which may not be resolved
177        infcx.tcx.erase_regions(ty),
178        into_iterator_did,
179    );
180
181    ocx.select_all_or_error().is_empty()
182}