Skip to main content

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

1use crate::{
2    Example, FileCtx, Violation,
3    infra::{parse, scanner},
4    violation,
5};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9    Example {
10        label: "unnamed event",
11        code: "event!(Level::INFO, foo = 1, \"msg\");",
12        pass: false,
13    },
14    Example {
15        label: "unnamed multi-line event",
16        code: "event!(\n    Level::INFO,\n    file.path = p,\n);",
17        pass: false,
18    },
19    Example {
20        label: "name after level",
21        code: "event!(Level::INFO, name: \"a.b.c\", \"msg\");",
22        pass: false,
23    },
24    Example {
25        label: "named event",
26        code: "event!(name: \"file.open.success\", Level::INFO, \"msg\");",
27        pass: true,
28    },
29    Example {
30        label: "named multi-line event",
31        code: "tracing::event!(\n    name: \"a.b.c\",\n    Level::INFO,\n);",
32        pass: true,
33    },
34    Example {
35        label: "event in comment",
36        code: "// event!(Level::INFO, \"msg\");",
37        pass: true,
38    },
39    Example {
40        label: "event in string literal",
41        code: "let s = \"event!(Level::INFO)\";",
42        pass: true,
43    },
44    Example {
45        label: "other macro named event",
46        code: "custom_event!(Level::INFO, \"msg\");",
47        pass: true,
48    },
49    Example {
50        label: "non-event log macro",
51        code: "info!(\"msg\");",
52        pass: true,
53    },
54];
55
56crate::line_rule!(
57    log_named_events,
58    "Flag `event!(...)` invocations without a `name:` argument before the level.",
59    "Unnamed events cannot be grouped or filtered across log entries; name them `<component>.<operation>.<state>`.",
60    Low,
61);
62
63const EVENT_OPEN: &str = "event!(";
64const MAX_SCAN_LINES: usize = 30;
65
66fn is_ident_char(ch: char) -> bool {
67    ch.is_ascii_alphanumeric() || ch == '_'
68}
69
70fn event_invocation_start(code: &str) -> Option<usize> {
71    let mut from = 0;
72
73    while let Some(pos) = code.get(from..).and_then(|rest| rest.find(EVENT_OPEN)) {
74        let abs = from + pos;
75        let prev = code.get(..abs).and_then(|before| before.chars().last());
76
77        if !prev.is_some_and(is_ident_char) {
78            return Some(abs + EVENT_OPEN.len());
79        }
80
81        from = abs + EVENT_OPEN.len();
82    }
83
84    None
85}
86
87fn consume(text: &str, depth: &mut usize, content: &mut String) -> bool {
88    for ch in text.chars() {
89        match ch {
90            '(' => *depth += 1,
91            ')' => {
92                *depth -= 1;
93
94                if *depth == 0 {
95                    return true;
96                }
97            }
98            _ => {}
99        }
100
101        if !ch.is_whitespace() {
102            content.push(ch);
103        }
104    }
105
106    false
107}
108
109fn has_name_before_level(content: &str) -> bool {
110    let Some(name_pos) = content.find("name:") else {
111        return false;
112    };
113
114    content
115        .find("Level::")
116        .is_none_or(|level_pos| name_pos < level_pos)
117}
118
119fn check_log_named_events(ctx: &FileCtx<'_>) -> Vec<Violation> {
120    let mut out = Vec::new();
121
122    for (i, line) in ctx.lines.iter().enumerate() {
123        if parse::is_comment(line.trim()) {
124            continue;
125        }
126
127        let code = scanner::code_only(line);
128        let Some(start) = event_invocation_start(&code) else {
129            continue;
130        };
131
132        let mut content = String::new();
133        let mut depth: usize = 1;
134        let mut done = consume(code.get(start..).unwrap_or(""), &mut depth, &mut content);
135        let mut next = i + 1;
136
137        while !done && next - i < MAX_SCAN_LINES {
138            let Some(next_line) = ctx.lines.get(next) else {
139                break;
140            };
141
142            done = consume(&scanner::code_only(next_line), &mut depth, &mut content);
143            next += 1;
144        }
145
146        if !has_name_before_level(&content) {
147            out.push(violation(
148                ctx.rel,
149                i + 1,
150                "event!() without a leading name: argument — name events `<component>.<operation>.<state>`",
151            ));
152        }
153    }
154
155    out
156}
157
158crate::tidy_test!(check_log_named_events, {
159    crate::example_tests!(EXAMPLES, check_log_named_events);
160});