Skip to main content

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

1use std::collections::VecDeque;
2
3use ra_ap_syntax::{AstNode, SyntaxElement, ast};
4
5use crate::{AstCtx, Example, Violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9    Example {
10        label: "positional placeholder in message",
11        code: "fn f(p: u32) { tracing::info!(\"file opened: {}\", p); }",
12        pass: false,
13    },
14    Example {
15        label: "named inline placeholder in message",
16        code: "fn f(p: u32) { info!(\"opened {p}\"); }",
17        pass: false,
18    },
19    Example {
20        label: "format! argument",
21        code: "fn f(p: u32) { warn!(msg = format!(\"opened\")); }",
22        pass: false,
23    },
24    Example {
25        label: "to_string argument",
26        code: "fn f(p: u32) { log::debug!(value = p.to_string()); }",
27        pass: false,
28    },
29    Example {
30        label: "message template braces pass",
31        code: "fn f(p: u32) { event!(name: \"file.open\", Level::INFO, file.path = p, \"file opened: {{file.path}}\"); }",
32        pass: true,
33    },
34    Example {
35        label: "plain message",
36        code: "fn f() { info!(\"cache flushed\"); }",
37        pass: true,
38    },
39    Example {
40        label: "structured fields",
41        code: "fn f(p: u32) { info!(count = p, \"batch done\"); }",
42        pass: true,
43    },
44    Example {
45        label: "format outside logging",
46        code: "fn f(p: u32) { let s = format!(\"x {}\", p); }",
47        pass: true,
48    },
49    Example {
50        label: "format in log in test module",
51        code: "#[cfg(test)]\nmod tests {\n    fn f(p: u32) { info!(\"opened {}\", p); }\n}",
52        pass: true,
53    },
54];
55
56crate::ast_rule!(
57    format_in_log,
58    "Flag runtime string building (`format!`, `.to_string()`, `{}` placeholders) in logging macros.",
59    "String formatting allocates on every emit even when the event is filtered out; structured fields with {{property}} message templates defer formatting to viewing time.",
60    Low,
61);
62
63const LOG_MACROS: &[&str] = &["debug", "error", "event", "info", "log", "trace", "warn"];
64
65fn string_content(lit: &str) -> Option<&str> {
66    let s = lit.strip_prefix('r').unwrap_or(lit);
67    let s = s.trim_start_matches('#');
68    let s = s.strip_prefix('"')?;
69    let s = s.trim_end_matches('#');
70
71    s.strip_suffix('"')
72}
73
74fn has_single_brace_placeholder(content: &str) -> bool {
75    let mut chars = content.chars().peekable();
76
77    while let Some(ch) = chars.next() {
78        match ch {
79            '{' => {
80                if chars.peek() == Some(&'{') {
81                    chars.next();
82                } else {
83                    return true;
84                }
85            }
86            '}' if chars.peek() == Some(&'}') => {
87                chars.next();
88            }
89            _ => {}
90        }
91    }
92
93    false
94}
95
96fn pair_hit(current: &SyntaxElement, next: &SyntaxElement) -> Option<&'static str> {
97    match (current.as_token(), next.as_token()) {
98        (Some(identifier), Some(punctuation))
99            if identifier.text() == "format" && punctuation.text() == "!" =>
100        {
101            Some(
102                "format!() inside a logging macro — use structured fields instead of runtime string building",
103            )
104        }
105        (Some(punctuation), Some(identifier))
106            if punctuation.text() == "." && identifier.text() == "to_string" =>
107        {
108            Some(
109                ".to_string() inside a logging macro — use structured fields instead of runtime string building",
110            )
111        }
112        _ => None,
113    }
114}
115
116fn literal_hit(text: &str, saw_first_string: &mut bool) -> Option<&'static str> {
117    if *saw_first_string {
118        return None;
119    }
120
121    let content = string_content(text)?;
122
123    *saw_first_string = true;
124
125    has_single_brace_placeholder(content).then_some("format placeholder in log message — use structured fields and {{property}} message templates")
126}
127
128fn scan_log_tokens(call: &ast::MacroCall) -> Option<&'static str> {
129    let mut queue = VecDeque::new();
130
131    queue.push_back(call.token_tree()?);
132    let mut saw_first_string = false;
133
134    while let Some(tree) = queue.pop_front() {
135        let level: Vec<SyntaxElement> = tree
136            .syntax()
137            .children_with_tokens()
138            .filter(|element| {
139                element
140                    .as_token()
141                    .is_none_or(|token| !token.kind().is_trivia())
142            })
143            .collect();
144
145        for (cur, next) in level.iter().zip(level.iter().skip(1)) {
146            if let Some(message) = pair_hit(cur, next) {
147                return Some(message);
148            }
149        }
150
151        for element in level {
152            match element {
153                SyntaxElement::Token(token) => {
154                    if let Some(message) = literal_hit(token.text(), &mut saw_first_string) {
155                        return Some(message);
156                    }
157                }
158                SyntaxElement::Node(node) => {
159                    if let Some(tree) = ast::TokenTree::cast(node) {
160                        queue.push_back(tree);
161                    }
162                }
163            }
164        }
165    }
166
167    None
168}
169
170fn check_format_in_log(ctx: &AstCtx<'_>) -> Vec<Violation> {
171    ctx.nodes::<ast::MacroCall>()
172        .filter(|call| !ctx.is_in_test(call))
173        .filter_map(|call| {
174            let name = call.path()?.segment()?.name_ref()?.text().to_string();
175
176            if !LOG_MACROS.contains(&name.as_str()) {
177                return None;
178            }
179
180            scan_log_tokens(&call).map(|message| ctx.violation(&call, message))
181        })
182        .collect()
183}
184
185crate::tidy_ast_test!(check_format_in_log, {
186    crate::example_tests!(EXAMPLES, check_format_in_log);
187});