use ra_ap_syntax::{
AstNode, ast,
ast::{HasGenericParams, HasName},
};
use crate::{AstCtx, Example, Violation};
#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
Example {
label: "pure arithmetic function",
code: "fn double(x: u32) -> u32 { x * 2 }",
pass: false,
},
Example {
label: "already const fn",
code: "const fn double(x: u32) -> u32 { x * 2 }",
pass: true,
},
Example {
label: "function with method calls",
code: "fn f(s: &str) -> usize { s.len() }",
pass: true,
},
Example {
label: "function with macro call",
code: r#"fn f() -> String { format!("hello") }"#,
pass: true,
},
Example {
label: "const candidate in test",
code: "#[cfg(test)]\nmod tests {\n fn double(x: u32) -> u32 { x * 2 }\n}",
pass: true,
},
Example {
label: "function with if-else on params",
code: "fn max_val(a: u32, b: u32) -> u32 { if a > b { a } else { b } }",
pass: false,
},
Example {
label: "function with loop",
code: "fn f() -> u32 { let mut i = 0; while i < 10 { i += 1; } i }",
pass: true,
},
Example {
label: "empty function",
code: "fn f() {}",
pass: true,
},
Example {
label: "unsafe function",
code: "unsafe fn f(x: u32) -> u32 { x * 2 }",
pass: true,
},
Example {
label: "function with where clause",
code: "fn f<T>(x: T) -> T where T: Copy { x }",
pass: true,
},
Example {
label: "function with impl Trait param",
code: "fn f(x: impl Into<u32>) -> u32 { 1 }",
pass: true,
},
Example {
label: "match with literal arms is const-eligible",
code: "fn f(a: u32) -> u32 { match a { _ => 1 } }",
pass: false,
},
Example {
label: "struct literal is const-eligible",
code: "fn f() -> Foo { Foo { x: 1 } }",
pass: false,
},
Example {
label: "index expression is const-eligible",
code: "fn f(a: [u32; 2]) -> u32 { a[0] }",
pass: false,
},
Example {
label: "field access is const-eligible",
code: "fn f(p: Point) -> u32 { p.x }",
pass: false,
},
Example {
label: "tuple is const-eligible",
code: "fn f(a: u32, b: u32) -> (u32, u32) { (a, b) }",
pass: false,
},
Example {
label: "cast is const-eligible",
code: "fn f(a: u32) -> u8 { a as u8 }",
pass: false,
},
Example {
label: "nested block is const-eligible",
code: "fn f() -> u32 { { 1 } }",
pass: false,
},
Example {
label: "return of literal is const-eligible",
code: "fn f() -> u32 { return 1; }",
pass: false,
},
Example {
label: "function call is not const-eligible",
code: "fn f() -> u32 { g() }",
pass: true,
},
Example {
label: "await is not const-eligible",
code: "fn f() -> u32 { a.await }",
pass: true,
},
];
crate::ast_rule!(
const_fn_candidate,
"Flag syntactically simple functions worth evaluating as `const fn` candidates.",
"Const eligibility depends on types, trait implementations, destructors, and the active toolchain. This rule only identifies candidates; the compiler must confirm any manual change.",
Low,
default = false,
);
fn check_const_fn_candidate(ctx: &AstCtx<'_>) -> Vec<Violation> {
let free_functions = ctx
.nodes::<ast::Fn>()
.filter(|function| !ctx.is_in_test(function) && is_free_function(function))
.filter(|function| {
function.const_token().is_none()
&& function.async_token().is_none()
&& function.unsafe_token().is_none()
&& function.generic_param_list().is_none()
&& function.where_clause().is_none()
&& !has_impl_trait_params(function)
&& function.ret_type().is_some()
&& function
.body()
.is_some_and(|body| is_const_eligible_block(&body))
});
free_functions
.filter_map(|function| {
let name = function.name()?;
Some(ctx.violation(
&name,
format!(
"`{name}` is a syntactically simple `const fn` candidate — verify type operations and destructors with the project compiler before changing it"
),
))
})
.collect()
}
fn is_const_eligible_block(block: &ast::BlockExpr) -> bool {
let blocks_nonempty = block
.syntax()
.descendants()
.filter_map(ast::BlockExpr::cast)
.all(|block| {
block.stmt_list().is_some_and(|list| {
list.statements().next().is_some() || list.tail_expr().is_some()
})
});
let no_items = !block
.syntax()
.descendants()
.filter_map(ast::Stmt::cast)
.any(|stmt| matches!(stmt, ast::Stmt::Item(_)));
blocks_nonempty
&& no_items
&& block
.syntax()
.descendants()
.filter_map(ast::Expr::cast)
.all(|expr| {
matches!(
expr,
ast::Expr::Literal(_)
| ast::Expr::PathExpr(_)
| ast::Expr::BinExpr(_)
| ast::Expr::PrefixExpr(_)
| ast::Expr::ParenExpr(_)
| ast::Expr::IfExpr(_)
| ast::Expr::BlockExpr(_)
| ast::Expr::CastExpr(_)
| ast::Expr::TupleExpr(_)
| ast::Expr::IndexExpr(_)
| ast::Expr::FieldExpr(_)
| ast::Expr::RecordExpr(_)
| ast::Expr::ReturnExpr(_)
| ast::Expr::MatchExpr(_)
)
})
}
fn has_impl_trait_params(function: &ast::Fn) -> bool {
function
.param_list()
.into_iter()
.flat_map(|params| params.params())
.any(|param| matches!(param.ty(), Some(ast::Type::ImplTraitType(_))))
}
fn is_free_function(function: &ast::Fn) -> bool {
function
.syntax()
.parent()
.is_none_or(|parent| !ast::AssocItemList::can_cast(parent.kind()))
}
crate::rulewright_ast_test!(check_const_fn_candidate, {
crate::example_tests!(EXAMPLES, check_const_fn_candidate);
#[test]
fn rule_is_diagnostic_only() {
let rule = inventory::iter::<crate::Rule>
.into_iter()
.find(|rule| rule.info.name == "rust_const_fn_candidate")
.expect("rust_const_fn_candidate is registered");
assert!(rule.fix.is_none());
}
});