Skip to main content

wowlab_tidy/languages/rust/rules/api/
fallible_in_iterator.rs

1use 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});