1 use crate::{
2 lints::{
3 ForLoopsOverFalliblesDiag, ForLoopsOverFalliblesLoopSub, ForLoopsOverFalliblesQuestionMark,
4 ForLoopsOverFalliblesSuggestion,
5 },
6 LateContext, LateLintPass, LintContext,
7 };
8
9 use hir::{Expr, Pat};
10 use rustc_hir as hir;
11 use rustc_infer::{infer::TyCtxtInferExt, traits::ObligationCause};
12 use rustc_middle::ty::{self, List};
13 use rustc_span::{sym, Span};
14 use rustc_trait_selection::traits::ObligationCtxt;
15
16 declare_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
46 declare_lint_pass!(ForLoopsOverFallibles => [FOR_LOOPS_OVER_FALLIBLES]);
47
48 impl<'tcx> LateLintPass<'tcx> for ForLoopsOverFallibles {
check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>)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 &ty::Adt(adt, substs) = ty.kind() else { return };
55
56 let (article, ty, var) = match adt.did() {
57 did if cx.tcx.is_diagnostic_item(sym::Option, did) => ("an", "Option", "Some"),
58 did if cx.tcx.is_diagnostic_item(sym::Result, did) => ("a", "Result", "Ok"),
59 _ => return,
60 };
61
62 let sub = if let Some(recv) = extract_iterator_next_call(cx, arg)
63 && let Ok(recv_snip) = cx.sess().source_map().span_to_snippet(recv.span)
64 {
65 ForLoopsOverFalliblesLoopSub::RemoveNext { suggestion: recv.span.between(arg.span.shrink_to_hi()), recv_snip }
66 } else {
67 ForLoopsOverFalliblesLoopSub::UseWhileLet { start_span: expr.span.with_hi(pat.span.lo()), end_span: pat.span.between(arg.span), var }
68 } ;
69 let question_mark = suggest_question_mark(cx, adt, substs, expr.span)
70 .then(|| ForLoopsOverFalliblesQuestionMark { suggestion: arg.span.shrink_to_hi() });
71 let suggestion = ForLoopsOverFalliblesSuggestion {
72 var,
73 start_span: expr.span.with_hi(pat.span.lo()),
74 end_span: pat.span.between(arg.span),
75 };
76
77 cx.emit_spanned_lint(
78 FOR_LOOPS_OVER_FALLIBLES,
79 arg.span,
80 ForLoopsOverFalliblesDiag { article, ty, sub, question_mark, suggestion },
81 );
82 }
83 }
84
extract_for_loop<'tcx>(expr: &Expr<'tcx>) -> Option<(&'tcx Pat<'tcx>, &'tcx Expr<'tcx>)>85 fn extract_for_loop<'tcx>(expr: &Expr<'tcx>) -> Option<(&'tcx Pat<'tcx>, &'tcx Expr<'tcx>)> {
86 if let hir::ExprKind::DropTemps(e) = expr.kind
87 && let hir::ExprKind::Match(iterexpr, [arm], hir::MatchSource::ForLoopDesugar) = e.kind
88 && let hir::ExprKind::Call(_, [arg]) = iterexpr.kind
89 && let hir::ExprKind::Loop(block, ..) = arm.body.kind
90 && let [stmt] = block.stmts
91 && let hir::StmtKind::Expr(e) = stmt.kind
92 && let hir::ExprKind::Match(_, [_, some_arm], _) = e.kind
93 && let hir::PatKind::Struct(_, [field], _) = some_arm.pat.kind
94 {
95 Some((field.pat, arg))
96 } else {
97 None
98 }
99 }
100
extract_iterator_next_call<'tcx>( cx: &LateContext<'_>, expr: &Expr<'tcx>, ) -> Option<&'tcx Expr<'tcx>>101 fn extract_iterator_next_call<'tcx>(
102 cx: &LateContext<'_>,
103 expr: &Expr<'tcx>,
104 ) -> Option<&'tcx Expr<'tcx>> {
105 // This won't work for `Iterator::next(iter)`, is this an issue?
106 if let hir::ExprKind::MethodCall(_, recv, _, _) = expr.kind
107 && cx.typeck_results().type_dependent_def_id(expr.hir_id) == cx.tcx.lang_items().next_fn()
108 {
109 Some(recv)
110 } else {
111 return None
112 }
113 }
114
suggest_question_mark<'tcx>( cx: &LateContext<'tcx>, adt: ty::AdtDef<'tcx>, substs: &List<ty::GenericArg<'tcx>>, span: Span, ) -> bool115 fn suggest_question_mark<'tcx>(
116 cx: &LateContext<'tcx>,
117 adt: ty::AdtDef<'tcx>,
118 substs: &List<ty::GenericArg<'tcx>>,
119 span: Span,
120 ) -> bool {
121 let Some(body_id) = cx.enclosing_body else { return false };
122 let Some(into_iterator_did) = cx.tcx.get_diagnostic_item(sym::IntoIterator) else { return false };
123
124 if !cx.tcx.is_diagnostic_item(sym::Result, adt.did()) {
125 return false;
126 }
127
128 // Check that the function/closure/constant we are in has a `Result` type.
129 // Otherwise suggesting using `?` may not be a good idea.
130 {
131 let ty = cx.typeck_results().expr_ty(&cx.tcx.hir().body(body_id).value);
132 let ty::Adt(ret_adt, ..) = ty.kind() else { return false };
133 if !cx.tcx.is_diagnostic_item(sym::Result, ret_adt.did()) {
134 return false;
135 }
136 }
137
138 let ty = substs.type_at(0);
139 let infcx = cx.tcx.infer_ctxt().build();
140 let ocx = ObligationCtxt::new(&infcx);
141
142 let body_def_id = cx.tcx.hir().body_owner_def_id(body_id);
143 let cause = ObligationCause::new(
144 span,
145 body_def_id,
146 rustc_infer::traits::ObligationCauseCode::MiscObligation,
147 );
148
149 ocx.register_bound(
150 cause,
151 cx.param_env,
152 // Erase any region vids from the type, which may not be resolved
153 infcx.tcx.erase_regions(ty),
154 into_iterator_did,
155 );
156
157 ocx.select_all_or_error().is_empty()
158 }
159