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