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