Skip to main content

rustc_lint/
for_loops_over_fallibles.rs

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