Skip to main content

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

1use ra_ap_syntax::{
2    AstNode, ast,
3    ast::{HasGenericParams, HasName},
4};
5
6use crate::{AstCtx, Example, Fix, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example {
11        label: "pure arithmetic function",
12        code: "fn double(x: u32) -> u32 { x * 2 }",
13        pass: false,
14    },
15    Example {
16        label: "already const fn",
17        code: "const fn double(x: u32) -> u32 { x * 2 }",
18        pass: true,
19    },
20    Example {
21        label: "function with method calls",
22        code: "fn f(s: &str) -> usize { s.len() }",
23        pass: true,
24    },
25    Example {
26        label: "function with macro call",
27        code: r#"fn f() -> String { format!("hello") }"#,
28        pass: true,
29    },
30    Example {
31        label: "const candidate in test",
32        code: "#[cfg(test)]\nmod tests {\n    fn double(x: u32) -> u32 { x * 2 }\n}",
33        pass: true,
34    },
35    Example {
36        label: "function with if-else on params",
37        code: "fn max_val(a: u32, b: u32) -> u32 { if a > b { a } else { b } }",
38        pass: false,
39    },
40    Example {
41        label: "function with loop",
42        code: "fn f() -> u32 { let mut i = 0; while i < 10 { i += 1; } i }",
43        pass: true,
44    },
45    Example {
46        label: "empty function",
47        code: "fn f() {}",
48        pass: true,
49    },
50    Example {
51        label: "unsafe function",
52        code: "unsafe fn f(x: u32) -> u32 { x * 2 }",
53        pass: true,
54    },
55    Example {
56        label: "function with where clause",
57        code: "fn f<T>(x: T) -> T where T: Copy { x }",
58        pass: true,
59    },
60    Example {
61        label: "function with impl Trait param",
62        code: "fn f(x: impl Into<u32>) -> u32 { 1 }",
63        pass: true,
64    },
65    Example {
66        label: "match with literal arms is const-eligible",
67        code: "fn f(a: u32) -> u32 { match a { _ => 1 } }",
68        pass: false,
69    },
70    Example {
71        label: "struct literal is const-eligible",
72        code: "fn f() -> Foo { Foo { x: 1 } }",
73        pass: false,
74    },
75    Example {
76        label: "index expression is const-eligible",
77        code: "fn f(a: [u32; 2]) -> u32 { a[0] }",
78        pass: false,
79    },
80    Example {
81        label: "field access is const-eligible",
82        code: "fn f(p: Point) -> u32 { p.x }",
83        pass: false,
84    },
85    Example {
86        label: "tuple is const-eligible",
87        code: "fn f(a: u32, b: u32) -> (u32, u32) { (a, b) }",
88        pass: false,
89    },
90    Example {
91        label: "cast is const-eligible",
92        code: "fn f(a: u32) -> u8 { a as u8 }",
93        pass: false,
94    },
95    Example {
96        label: "nested block is const-eligible",
97        code: "fn f() -> u32 { { 1 } }",
98        pass: false,
99    },
100    Example {
101        label: "return of literal is const-eligible",
102        code: "fn f() -> u32 { return 1; }",
103        pass: false,
104    },
105    Example {
106        label: "function call is not const-eligible",
107        code: "fn f() -> u32 { g() }",
108        pass: true,
109    },
110    Example {
111        label: "await is not const-eligible",
112        code: "fn f() -> u32 { a.await }",
113        pass: true,
114    },
115];
116
117crate::ast_rule!(
118    const_fn_candidate,
119    "Flag pure functions that could be `const fn`.",
120    "Functions that only use const-compatible operations can be const fn, enabling compile-time evaluation.",
121    Low,
122    fix_const_fn_candidate,
123);
124
125fn check_const_fn_candidate(ctx: &AstCtx<'_>) -> Vec<Violation> {
126    let free_functions = ctx
127        .nodes::<ast::Fn>()
128        .filter(|function| !ctx.is_in_test(function) && is_free_function(function))
129        .filter(|function| {
130            function.const_token().is_none()
131                && function.async_token().is_none()
132                && function.unsafe_token().is_none()
133                && function.generic_param_list().is_none()
134                && function.where_clause().is_none()
135                && !has_impl_trait_params(function)
136                && function.ret_type().is_some()
137                && function
138                    .body()
139                    .is_some_and(|body| is_const_eligible_block(&body))
140        });
141
142    free_functions
143        .filter_map(|function| {
144            let name = function.name()?;
145
146            Some(ctx.violation(
147                &name,
148                format!(
149                    "`{name}` could be a `const fn` — it only uses const-compatible operations"
150                ),
151            ))
152        })
153        .collect()
154}
155
156fn is_const_eligible_block(block: &ast::BlockExpr) -> bool {
157    let blocks_nonempty = block
158        .syntax()
159        .descendants()
160        .filter_map(ast::BlockExpr::cast)
161        .all(|block| {
162            block.stmt_list().is_some_and(|list| {
163                list.statements().next().is_some() || list.tail_expr().is_some()
164            })
165        });
166    let no_items = !block
167        .syntax()
168        .descendants()
169        .filter_map(ast::Stmt::cast)
170        .any(|stmt| matches!(stmt, ast::Stmt::Item(_)));
171
172    blocks_nonempty
173        && no_items
174        && block
175            .syntax()
176            .descendants()
177            .filter_map(ast::Expr::cast)
178            .all(|expr| {
179                matches!(
180                    expr,
181                    ast::Expr::Literal(_)
182                        | ast::Expr::PathExpr(_)
183                        | ast::Expr::BinExpr(_)
184                        | ast::Expr::PrefixExpr(_)
185                        | ast::Expr::ParenExpr(_)
186                        | ast::Expr::IfExpr(_)
187                        | ast::Expr::BlockExpr(_)
188                        | ast::Expr::CastExpr(_)
189                        | ast::Expr::TupleExpr(_)
190                        | ast::Expr::IndexExpr(_)
191                        | ast::Expr::FieldExpr(_)
192                        | ast::Expr::RecordExpr(_)
193                        | ast::Expr::ReturnExpr(_)
194                        | ast::Expr::MatchExpr(_)
195                )
196            })
197}
198
199fn has_impl_trait_params(function: &ast::Fn) -> bool {
200    function
201        .param_list()
202        .into_iter()
203        .flat_map(|params| params.params())
204        .any(|param| matches!(param.ty(), Some(ast::Type::ImplTraitType(_))))
205}
206
207fn is_free_function(function: &ast::Fn) -> bool {
208    function
209        .syntax()
210        .parent()
211        .is_none_or(|parent| !ast::AssocItemList::can_cast(parent.kind()))
212}
213
214fn fix_const_fn_candidate(ctx: &AstCtx<'_>, v: &Violation) -> Option<Fix> {
215    let line = ctx.file.line(v.line)?;
216
217    Some(Fix::replace_line(
218        v.line,
219        line.replacen("fn ", "const fn ", 1),
220    ))
221}
222
223crate::tidy_ast_test!(check_const_fn_candidate, {
224    crate::example_tests!(EXAMPLES, check_const_fn_candidate);
225    crate::fix_tests!(ast, check_const_fn_candidate, fix_const_fn_candidate);
226});