wowlab_tidy/languages/rust/rules/api/
fallible_in_iterator.rs1use ra_ap_syntax::{AstNode, ast};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7 Example {
8 label: "unwrap in map closure",
9 code: r"fn f(v: Vec<Option<i32>>) -> Vec<i32> { v.into_iter().map(|x| x.unwrap()).collect() }",
10 pass: false,
11 },
12 Example {
13 label: "expect in filter_map",
14 code: r#"fn f(v: Vec<&str>) -> Vec<i32> { v.iter().filter_map(|x| Some(x.parse::<i32>().expect("bad"))).collect() }"#,
15 pass: false,
16 },
17 Example {
18 label: "unwrap in for_each",
19 code: r"fn f(v: Vec<Option<i32>>) { v.iter().for_each(|x| { x.unwrap(); }); }",
20 pass: false,
21 },
22 Example {
23 label: "no unwrap in map",
24 code: "fn f(v: Vec<i32>) -> Vec<i32> { v.into_iter().map(|x| x + 1).collect() }",
25 pass: true,
26 },
27 Example {
28 label: "unwrap outside iterator",
29 code: "fn f() { Some(1).unwrap(); }",
30 pass: true,
31 },
32 Example {
33 label: "unwrap in iterator in test",
34 code: "#[cfg(test)]\nmod tests {\n fn t(v: Vec<Option<i32>>) { v.into_iter().map(|x| x.unwrap()); }\n}",
35 pass: true,
36 },
37];
38
39crate::ast_rule!(
40 fallible_in_iterator,
41 "Flag `.unwrap()`/`.expect()` inside iterator adapter closures.",
42 "unwrap/expect inside iterator adapters panics mid-iteration with no recovery. Use filter_map or collect::<Result<_>>.",
43 Medium,
44);
45
46fn check_fallible_in_iterator(ctx: &AstCtx<'_>) -> Vec<Violation> {
47 ctx.nodes::<ast::MethodCallExpr>()
48 .filter(|call| !ctx.is_in_test(call))
49 .filter_map(|call| {
50 let method = call.name_ref()?;
51
52 if !matches!(method.text().as_str(), "unwrap" | "expect")
53 || !inside_iterator_closure(&call)
54 {
55 return None;
56 }
57
58 Some(ctx.violation(
59 &method,
60 format!(
61 ".{}() inside iterator adapter — use ? or filter_map instead",
62 method.text()
63 ),
64 ))
65 })
66 .collect()
67}
68
69const ITERATOR_METHODS: &[&str] = &[
70 "map",
71 "filter",
72 "filter_map",
73 "flat_map",
74 "for_each",
75 "find",
76 "find_map",
77 "any",
78 "all",
79 "inspect",
80 "scan",
81 "fold",
82 "reduce",
83 "try_fold",
84 "try_for_each",
85 "partition",
86];
87
88fn inside_iterator_closure(call: &ast::MethodCallExpr) -> bool {
89 let closure = call
90 .syntax()
91 .ancestors()
92 .skip(1)
93 .find_map(ast::ClosureExpr::cast);
94 let adapter = closure.and_then(|closure| {
95 closure
96 .syntax()
97 .ancestors()
98 .skip(1)
99 .find_map(ast::MethodCallExpr::cast)
100 });
101
102 adapter
103 .and_then(|adapter| adapter.name_ref())
104 .is_some_and(|name| ITERATOR_METHODS.contains(&name.text().as_str()))
105}
106
107crate::tidy_ast_test!(check_fallible_in_iterator, {
108 crate::example_tests!(EXAMPLES, check_fallible_in_iterator);
109});