1use hir::{Expr, Pat};
2use rustc_hir::{self as hir, LangItem};
3use rustc_infer::infer::TyCtxtInferExt;
4use rustc_infer::traits::ObligationCause;
5use rustc_middle::ty;
6use rustc_session::{declare_lint, declare_lint_pass};
7use rustc_span::{Span, sym};
8use rustc_trait_selection::traits::ObligationCtxt;
9
10use crate::lints::{
11 ForLoopsOverFalliblesDiag, ForLoopsOverFalliblesLoopSub, ForLoopsOverFalliblesQuestionMark,
12 ForLoopsOverFalliblesSuggestion,
13};
14use crate::{LateContext, LateLintPass, LintContext};
15
16declare_lint! {
17 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
46declare_lint_pass!(ForLoopsOverFallibles => [FOR_LOOPS_OVER_FALLIBLES]);
47
48impl<'tcx> LateLintPass<'tcx> for ForLoopsOverFallibles {
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 arg_span = arg.span.source_callsite();
53
54 let ty = cx.typeck_results().expr_ty(arg);
55
56 let (adt, args, ref_mutability) = match ty.kind() {
57 &ty::Adt(adt, args) => (adt, args, None),
58 &ty::Ref(_, ty, mutability) => match ty.kind() {
59 &ty::Adt(adt, args) => (adt, args, Some(mutability)),
60 _ => return,
61 },
62 _ => return,
63 };
64
65 let (article, ty, var) = match adt.did() {
66 did if cx.tcx.is_diagnostic_item(sym::Option, did) && ref_mutability.is_some() => {
67 ("a", "Option", "Some")
68 }
69 did if cx.tcx.is_diagnostic_item(sym::Option, did) => ("an", "Option", "Some"),
70 did if cx.tcx.is_diagnostic_item(sym::Result, did) => ("a", "Result", "Ok"),
71 _ => return,
72 };
73
74 let ref_prefix = match ref_mutability {
75 None => "",
76 Some(ref_mutability) => ref_mutability.ref_prefix_str(),
77 };
78
79 let sub = if let Some(recv) = extract_iterator_next_call(cx, arg)
80 && let Ok(recv_snip) = cx.sess().source_map().span_to_snippet(recv.span)
81 {
82 ForLoopsOverFalliblesLoopSub::RemoveNext {
83 suggestion: recv.span.between(arg_span.shrink_to_hi()),
84 recv_snip,
85 }
86 } else {
87 ForLoopsOverFalliblesLoopSub::UseWhileLet {
88 start_span: expr.span.with_hi(pat.span.lo()),
89 end_span: pat.span.between(arg_span),
90 var,
91 }
92 };
93 let question_mark = suggest_question_mark(cx, adt, args, expr.span)
94 .then(|| ForLoopsOverFalliblesQuestionMark { suggestion: arg_span.shrink_to_hi() });
95 let suggestion = ForLoopsOverFalliblesSuggestion {
96 var,
97 start_span: expr.span.with_hi(pat.span.lo()),
98 end_span: pat.span.between(arg_span),
99 };
100
101 cx.emit_span_lint(
102 FOR_LOOPS_OVER_FALLIBLES,
103 arg_span,
104 ForLoopsOverFalliblesDiag { article, ref_prefix, ty, sub, question_mark, suggestion },
105 );
106 }
107}
108
109fn extract_for_loop<'tcx>(expr: &Expr<'tcx>) -> Option<(&'tcx Pat<'tcx>, &'tcx Expr<'tcx>)> {
110 if let hir::ExprKind::DropTemps(e) = expr.kind
111 && let hir::ExprKind::Match(iterexpr, [arm], hir::MatchSource::ForLoopDesugar) = e.kind
112 && let hir::ExprKind::Call(_, [arg]) = iterexpr.kind
113 && let hir::ExprKind::Loop(block, ..) = arm.body.kind
114 && let [stmt] = block.stmts
115 && let hir::StmtKind::Expr(e) = stmt.kind
116 && let hir::ExprKind::Match(_, [_, some_arm], _) = e.kind
117 && let hir::PatKind::Struct(_, [field], _) = some_arm.pat.kind
118 {
119 Some((field.pat, arg))
120 } else {
121 None
122 }
123}
124
125fn extract_iterator_next_call<'tcx>(
126 cx: &LateContext<'_>,
127 expr: &Expr<'tcx>,
128) -> Option<&'tcx Expr<'tcx>> {
129 if let hir::ExprKind::MethodCall(_, recv, _, _) = expr.kind
131 && cx
132 .typeck_results()
133 .type_dependent_def_id(expr.hir_id)
134 .is_some_and(|def_id| cx.tcx.is_lang_item(def_id, LangItem::IteratorNext))
135 {
136 Some(recv)
137 } else {
138 None
139 }
140}
141
142fn suggest_question_mark<'tcx>(
143 cx: &LateContext<'tcx>,
144 adt: ty::AdtDef<'tcx>,
145 args: ty::GenericArgsRef<'tcx>,
146 span: Span,
147) -> bool {
148 let Some(body_id) = cx.enclosing_body else { return false };
149 let Some(into_iterator_did) = cx.tcx.get_diagnostic_item(sym::IntoIterator) else {
150 return false;
151 };
152
153 if !cx.tcx.is_diagnostic_item(sym::Result, adt.did()) {
154 return false;
155 }
156
157 {
160 let ty = cx.typeck_results().expr_ty(cx.tcx.hir_body(body_id).value);
161 let ty::Adt(ret_adt, ..) = ty.kind() else { return false };
162 if !cx.tcx.is_diagnostic_item(sym::Result, ret_adt.did()) {
163 return false;
164 }
165 }
166
167 let ty = args.type_at(0);
168 let (infcx, param_env) = cx.tcx.infer_ctxt().build_with_typing_env(cx.typing_env());
169 let ocx = ObligationCtxt::new(&infcx);
170
171 let body_def_id = cx.tcx.hir_body_owner_def_id(body_id);
172 let cause =
173 ObligationCause::new(span, body_def_id, rustc_infer::traits::ObligationCauseCode::Misc);
174
175 ocx.register_bound(
176 cause,
177 param_env,
178 infcx.tcx.erase_regions(ty),
180 into_iterator_did,
181 );
182
183 ocx.select_all_or_error().is_empty()
184}