wowlab_tidy/languages/rust/rules/performance/
alloc_in_loop.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{AstNode, ast};
4
5use super::support;
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "format! in for loop",
12 code: r#"fn f() { for i in 0..10 { let _ = format!("item {}", i); } }"#,
13 pass: false,
14 },
15 Example {
16 label: "to_string in while loop",
17 code: "fn f() { let mut i = 0; while i < 10 { let _ = i.to_string(); i += 1; } }",
18 pass: false,
19 },
20 Example {
21 label: "push_str in loop",
22 code: "fn f() { let mut s = String::new(); for _ in 0..10 { s.push_str(\"x\"); } }",
23 pass: false,
24 },
25 Example {
26 label: "format! outside loop",
27 code: r#"fn f() { let _ = format!("hello"); }"#,
28 pass: true,
29 },
30 Example {
31 label: "to_string outside loop",
32 code: "fn f() { let _ = 42.to_string(); }",
33 pass: true,
34 },
35 Example {
36 label: "push_str outside loop",
37 code: "fn f() { let mut s = String::new(); s.push_str(\"x\"); }",
38 pass: true,
39 },
40 Example {
41 label: "format! in loop in test",
42 code: "#[cfg(test)]\nmod tests {\n fn t() { for i in 0..10 { let _ = format!(\"x{}\", i); } }\n}",
43 pass: true,
44 },
45];
46
47crate::ast_rule!(
48 alloc_in_loop,
49 "Flag `format!()`, `format_args!()`, `.to_string()`, and `.push_str()` inside loops.",
50 "These allocate or format a new String each iteration. Pre-allocate, use write! to a buffer, or collect and join.",
51 Medium,
52);
53
54fn check_alloc_in_loop(ctx: &AstCtx<'_>) -> Vec<Violation> {
55 let methods = ctx
56 .nodes::<ast::MethodCallExpr>()
57 .filter(|call| {
58 !ctx.is_in_test(call) && support::is_inside_loop_body(call) && !inside_diagnostic(call)
59 })
60 .filter_map(|call| {
61 let method = support::method_name(&call)?;
62 let message = if method == "to_string" && support::has_no_args(&call) {
63 ".to_string() inside a loop — allocates each iteration"
64 } else if method == "push_str" {
65 ".push_str() inside a loop — consider pre-allocating or collecting and joining"
66 } else {
67 return None;
68 };
69
70 Some(ctx.violation(&call, message))
71 });
72 let macros = ctx
73 .nodes::<ast::MacroCall>()
74 .filter(|call| {
75 !ctx.is_in_test(call) && support::is_inside_loop_body(call) && !inside_diagnostic(call)
76 })
77 .filter_map(|call| {
78 let path = call.path()?;
79
80 if path.qualifier().is_some() {
81 return None;
82 }
83
84 let name = path.segment()?.name_ref()?.text().to_string();
85
86 matches!(name.as_str(), "format" | "format_args")
87 .then(|| ctx.violation(&call, "format!() inside a loop — allocates each iteration"))
88 });
89
90 methods.chain(macros).collect()
91}
92
93fn inside_diagnostic<N>(node: &N) -> bool
94where
95 N: AstNode,
96{
97 node.syntax().ancestors().skip(1).any(|ancestor| {
98 if let Some(call) = ast::MethodCallExpr::cast(ancestor.clone()) {
99 return call
100 .name_ref()
101 .is_some_and(|name| name.text() == "violation");
102 }
103
104 let function = ast::CallExpr::cast(ancestor)
105 .and_then(|call| call.expr())
106 .and_then(|expr| support::path_expr_name(&expr));
107
108 function.is_some_and(|name| name == "violation")
109 })
110}
111
112crate::tidy_ast_test!(check_alloc_in_loop, {
113 crate::example_tests!(EXAMPLES, check_alloc_in_loop);
114
115 #[gtest]
116 fn diagnostic_messages_may_be_built_in_loops() -> Result<()> {
117 let source = r#"fn check(ctx: &Ctx) {
118 for name in names {
119 out.push(ctx.violation(format!("bad {name}")));
120 out.push(violation(path, format!("bad {name}")));
121 }
122 }"#;
123 verify_true!(run(source).is_empty())?;
124
125 Ok(())
126 }
127});