Skip to main content

wowlab_tidy/languages/rust/rules/hygiene/
log_in_loop.rs

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