wowlab_tidy/languages/rust/rules/hygiene/
log_in_loop.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self, HasLoopBody},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "info in for loop",
12 code: "fn f(v: Vec<u32>) { for m in v { tracing::info!(\"processing\"); } }",
13 pass: false,
14 },
15 Example {
16 label: "warn in while loop",
17 code: "fn f() { while running() { warn!(\"still waiting\"); } }",
18 pass: false,
19 },
20 Example {
21 label: "event in loop",
22 code: "fn f() { loop { event!(Level::TRACE, \"tick\"); } }",
23 pass: false,
24 },
25 Example {
26 label: "debug in nested block in loop",
27 code: "fn f(v: Vec<u32>) { for m in v { if m > 0 { log::debug!(\"item\"); } } }",
28 pass: false,
29 },
30 Example {
31 label: "log outside loop",
32 code: "fn f() { tracing::info!(\"batch done\"); }",
33 pass: true,
34 },
35 Example {
36 label: "batch event before loop",
37 code: "fn f(v: Vec<u32>) { info!(\"processing batch\"); for m in v { handle(m); } }",
38 pass: true,
39 },
40 Example {
41 label: "non-log macro in loop",
42 code: "fn f(v: Vec<u32>) { for m in v { black_box!(m); } }",
43 pass: true,
44 },
45 Example {
46 label: "log in loop in test module",
47 code: "#[cfg(test)]\nmod tests {\n fn f(v: Vec<u32>) { for m in v { tracing::info!(\"x\"); } }\n}",
48 pass: true,
49 },
50];
51
52crate::ast_rule!(
53 log_in_loop,
54 "Flag logging macro invocations inside loop bodies in library code.",
55 "Per-iteration telemetry turns hot loops into allocation and I/O hotspots; emit one batch-level event before or after the loop instead.",
56 Low,
57);
58
59const LOG_MACROS: &[&str] = &["debug", "error", "event", "info", "log", "trace", "warn"];
60
61fn macro_name(call: &ast::MacroCall) -> Option<String> {
62 call.path()?
63 .segment()?
64 .name_ref()
65 .map(|name| name.text().to_string())
66}
67
68fn check_log_in_loop(ctx: &AstCtx<'_>) -> Vec<Violation> {
69 let log_calls = ctx
70 .nodes::<ast::MacroCall>()
71 .filter(|call| !ctx.is_in_test(call))
72 .filter(|call| macro_name(call).is_some_and(|name| LOG_MACROS.contains(&name.as_str())));
73
74 log_calls
75 .filter(is_inside_loop)
76 .map(|call| {
77 ctx.violation(
78 &call,
79 "logging macro inside a loop — emit one batch-level event instead of per-iteration telemetry",
80 )
81 })
82 .collect()
83}
84
85fn is_inside_loop(call: &ast::MacroCall) -> bool {
86 call.syntax().ancestors().skip(1).any(|ancestor| {
87 if let Some(for_expr) = ast::ForExpr::cast(ancestor.clone()) {
88 return for_expr.loop_body().is_some_and(|body| {
89 body.syntax()
90 .text_range()
91 .contains_range(call.syntax().text_range())
92 });
93 }
94
95 ast::WhileExpr::can_cast(ancestor.kind()) || ast::LoopExpr::can_cast(ancestor.kind())
96 })
97}
98
99crate::tidy_ast_test!(check_log_in_loop, {
100 crate::example_tests!(EXAMPLES, check_log_in_loop);
101});