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