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