wowlab_tidy/languages/rust/rules/performance/
ok_or_eager.rs1use 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});