• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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