Skip to main content

wowlab_tidy/languages/rust/rules/performance/
ok_or_eager.rs

1use ra_ap_syntax::{ast, ast::HasArgList};
2
3use crate::{AstCtx, Example, Fix, Violation, infra::parse};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "ok_or with call",
9        code: "fn f() { None::<i32>.ok_or(String::new()); }",
10        pass: false,
11    },
12    Example {
13        label: "ok_or with path",
14        code: "fn f() { None::<i32>.ok_or(MyError::Static); }",
15        pass: true,
16    },
17    Example {
18        label: "ok_or with struct literal",
19        code: "fn f() { None::<i32>.ok_or(MyError { id: 1 }); }",
20        pass: true,
21    },
22    Example {
23        label: "unwrap_or with call",
24        code: "fn f() { None::<String>.unwrap_or(String::new()); }",
25        pass: false,
26    },
27    Example {
28        label: "unwrap_or with literal",
29        code: "fn f() { None::<i32>.unwrap_or(0); }",
30        pass: true,
31    },
32    Example {
33        label: "unwrap_or with len",
34        code: "fn f(v: &[u8]) { v.iter().position(|&b| b == 0).unwrap_or(v.len()); }",
35        pass: true,
36    },
37    Example {
38        label: "ok_or in test module",
39        code: "#[cfg(test)]\nmod tests {\n    fn t() { None::<i32>.ok_or(String::new()); }\n}",
40        pass: true,
41    },
42];
43
44crate::ast_rule!(
45    ok_or_eager,
46    "Flag `.ok_or()`/`.unwrap_or()` with eagerly evaluated arguments.",
47    "ok_or() and unwrap_or() eagerly evaluate their argument even on the happy path. Use the _else variant for expensive expressions.",
48    Low,
49    fix_ok_or_eager,
50);
51
52fn check_ok_or_eager(ctx: &AstCtx<'_>) -> Vec<Violation> {
53    ctx.nodes::<ast::MethodCallExpr>()
54        .filter(|call| !ctx.is_in_test(call))
55        .filter_map(|call| {
56            let method = call.name_ref()?.text().to_string();
57
58            if method != "ok_or" && method != "unwrap_or" {
59                return None;
60            }
61
62            let arguments = call.arg_list()?;
63            let mut args = arguments.args();
64            let argument = args.next()?;
65
66            if args.next().is_some() || !is_eager_expr(&argument) {
67                return None;
68            }
69
70            Some(ctx.violation(
71                &call,
72                format!(
73                    ".{method}() with eagerly evaluated argument — use .{method}_else(|| ...) instead"
74                ),
75            ))
76        })
77        .collect()
78}
79
80fn is_eager_expr(expr: &ast::Expr) -> bool {
81    match expr {
82        ast::Expr::CallExpr(_) | ast::Expr::MacroExpr(_) => true,
83        ast::Expr::MethodCallExpr(call) => {
84            let Some(name) = call.name_ref().map(|name| name.text().to_string()) else {
85                return false;
86            };
87
88            !matches!(
89                name.as_str(),
90                "len" | "is_empty" | "clone" | "to_owned" | "to_string" | "into"
91            )
92        }
93        _ => false,
94    }
95}
96
97fn fix_ok_or_eager(ctx: &AstCtx<'_>, v: &Violation) -> Option<Fix> {
98    let line = ctx.file.line(v.line)?;
99
100    ["ok_or", "unwrap_or"].iter().find_map(|method| {
101        let pattern = format!(".{method}(");
102        let (before, inner, after) = parse::balanced_extract(line, &pattern)?;
103
104        Some(Fix::replace_line(
105            v.line,
106            format!("{before}.{method}_else(|| {inner}){after}"),
107        ))
108    })
109}
110
111crate::tidy_ast_test!(check_ok_or_eager, {
112    crate::example_tests!(EXAMPLES, check_ok_or_eager);
113    crate::fix_tests!(ast, check_ok_or_eager, fix_ok_or_eager);
114});