Skip to main content

wowlab_tidy/languages/rust/rules/performance/
alloc_in_loop.rs

1#[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});