Skip to main content

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

1use ra_ap_syntax::{AstNode, ast, ast::HasArgList};
2
3use super::support;
4use crate::{AstCtx, Example, Violation};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8    Example {
9        label: "Vec::new inside for loop, receiver-only use",
10        code: "fn f(n: usize) { for _ in 0..n { let mut buf = Vec::new(); buf.push(1); } }",
11        pass: false,
12    },
13    Example {
14        label: "String::new inside while loop",
15        code: "fn f(n: usize) { let mut i = 0; while i < n { let mut s = String::new(); s.push('x'); i += 1; } }",
16        pass: false,
17    },
18    Example {
19        label: "vec! macro inside loop",
20        code: "fn f(n: usize) { for _ in 0..n { let v = vec![1, 2]; let _ = v.len(); } }",
21        pass: false,
22    },
23    Example {
24        label: "with_capacity inside loop",
25        code: "fn f(n: usize) { for _ in 0..n { let mut m = std::collections::HashMap::with_capacity(8); m.insert(1, 1); } }",
26        pass: false,
27    },
28    Example {
29        label: "binding in nested block inside loop",
30        code: "fn f(xs: &[u32]) { for x in xs { if *x > 0 { let mut v = Vec::new(); v.push(*x); } } }",
31        pass: false,
32    },
33    Example {
34        label: "return escape still flagged (approximation: only call-argument use counts as escaping)",
35        code: "fn f(n: usize) -> Vec<u32> { for _ in 0..n { let v = Vec::new(); if !v.is_empty() { return v; } } Vec::new() }",
36        pass: false,
37    },
38    Example {
39        label: "escapes as method-call argument into outer collection",
40        code: "fn f(n: usize, out: &mut Vec<Vec<u32>>) { for i in 0..n { let mut row = Vec::new(); row.push(i as u32); out.push(row); } }",
41        pass: true,
42    },
43    Example {
44        label: "escapes as plain function-call argument",
45        code: "fn g(v: Vec<u32>) { drop(v); } fn f(n: usize) { for _ in 0..n { let v = Vec::new(); g(v); } }",
46        pass: true,
47    },
48    Example {
49        label: "hoisted allocation cleared per iteration",
50        code: "fn f(n: usize) { let mut buf = Vec::new(); for _ in 0..n { buf.push(1); buf.clear(); } }",
51        pass: true,
52    },
53    Example {
54        label: "constructor outside any loop",
55        code: "fn f() { let mut v = Vec::new(); v.push(1); }",
56        pass: true,
57    },
58    Example {
59        label: "constructor in loop in test module",
60        code: "#[cfg(test)]\nmod tests {\n    fn t(n: usize) { for _ in 0..n { let mut v = Vec::new(); v.push(1); } }\n}",
61        pass: true,
62    },
63];
64
65crate::ast_rule!(
66    collection_new_in_loop,
67    "Flag collection constructors (`Vec::new()`, `vec![]`, `with_capacity`, ...) bound via `let` inside loops.",
68    "Allocating a fresh collection per iteration is invisible overhead — hoist it out of the loop and .clear() each round.",
69    Medium,
70);
71
72fn check_collection_new_in_loop(ctx: &AstCtx<'_>) -> Vec<Violation> {
73    ctx.nodes::<ast::LetStmt>()
74        .filter(|local| !ctx.is_in_test(local) && support::is_inside_loop_body(local))
75        .filter_map(|local| {
76            let initializer = local.initializer()?;
77
78            if !is_collection_ctor(&initializer) {
79                return None;
80            }
81
82            let name = local_ident(local.pat()?)?;
83            let block = local.syntax().parent().and_then(ast::StmtList::cast)?;
84
85            (!escapes_from_block(&block, &name)).then(|| ctx.violation(&local, MSG))
86        })
87        .collect()
88}
89
90const COLLECTION_TYPES: &[&str] = &[
91    "BTreeMap", "HashMap", "HashSet", "String", "Vec", "VecDeque",
92];
93const MIN_CTOR_SEGMENTS: usize = 2;
94
95const MSG: &str = "collection allocated inside a loop — hoist it out and .clear() per iteration (escape check is best-effort: only call-argument use counts as escaping)";
96
97fn is_collection_ctor(expr: &ast::Expr) -> bool {
98    match expr {
99        ast::Expr::CallExpr(call) => {
100            let Some(ast::Expr::PathExpr(function)) = call.expr() else {
101                return false;
102            };
103            let Some(path) = function.path() else {
104                return false;
105            };
106            let names = support::path_names(&path);
107            let Some(last) = names.last() else {
108                return false;
109            };
110
111            if last == "with_capacity" {
112                return names.len() >= MIN_CTOR_SEGMENTS;
113            }
114
115            if last == "new" && support::has_no_args(call) {
116                return names
117                    .iter()
118                    .rev()
119                    .nth(1)
120                    .is_some_and(|ty| COLLECTION_TYPES.contains(&ty.as_str()));
121            }
122
123            false
124        }
125        ast::Expr::MacroExpr(mac) => {
126            mac.macro_call()
127                .and_then(|call| call.path())
128                .is_some_and(|path| {
129                    path.qualifier().is_none()
130                        && path
131                            .segment()
132                            .and_then(|segment| segment.name_ref())
133                            .is_some_and(|name| name.text() == "vec")
134                })
135        }
136        _ => false,
137    }
138}
139
140fn local_ident(pattern: ast::Pat) -> Option<String> {
141    support::ident_pattern_name(pattern)
142}
143
144fn escapes_from_block(block: &ast::StmtList, name: &str) -> bool {
145    let plain_calls = block.syntax().descendants().filter_map(ast::CallExpr::cast);
146    let method_calls = block
147        .syntax()
148        .descendants()
149        .filter_map(ast::MethodCallExpr::cast);
150
151    let mut arguments = plain_calls
152        .filter_map(|call| call.arg_list())
153        .chain(method_calls.filter_map(|call| call.arg_list()))
154        .flat_map(|arguments| arguments.args());
155
156    arguments.any(|argument| support::syntax_contains_ident(argument.syntax(), name))
157}
158
159crate::tidy_ast_test!(check_collection_new_in_loop, {
160    crate::example_tests!(EXAMPLES, check_collection_new_in_loop);
161});