rulewright 0.1.1

Executable engineering standards for Rust workspaces
Documentation
use crate::{
    Example, FileCtx, Violation,
    infra::{parse, scanner},
    violation,
};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "unnamed event",
        code: "event!(Level::INFO, foo = 1, \"msg\");",
        pass: false,
    },
    Example {
        label: "unnamed multi-line event",
        code: "event!(\n    Level::INFO,\n    file.path = p,\n);",
        pass: false,
    },
    Example {
        label: "name after level",
        code: "event!(Level::INFO, name: \"a.b.c\", \"msg\");",
        pass: false,
    },
    Example {
        label: "named event",
        code: "event!(name: \"file.open.success\", Level::INFO, \"msg\");",
        pass: true,
    },
    Example {
        label: "named multi-line event",
        code: "tracing::event!(\n    name: \"a.b.c\",\n    Level::INFO,\n);",
        pass: true,
    },
    Example {
        label: "event in comment",
        code: "// event!(Level::INFO, \"msg\");",
        pass: true,
    },
    Example {
        label: "event in string literal",
        code: "let s = \"event!(Level::INFO)\";",
        pass: true,
    },
    Example {
        label: "other macro named event",
        code: "custom_event!(Level::INFO, \"msg\");",
        pass: true,
    },
    Example {
        label: "non-event log macro",
        code: "info!(\"msg\");",
        pass: true,
    },
];

crate::line_rule!(
    log_named_events,
    "Flag `event!(...)` invocations without a `name:` argument before the level.",
    "Unnamed events cannot be grouped or filtered across log entries; name them `<component>.<operation>.<state>`.",
    Low,
);

const EVENT_OPEN: &str = "event!(";
const MAX_SCAN_LINES: usize = 30;

fn is_ident_char(ch: char) -> bool {
    ch.is_ascii_alphanumeric() || ch == '_'
}

fn event_invocation_start(code: &str) -> Option<usize> {
    let mut from = 0;

    while let Some(pos) = code.get(from..).and_then(|rest| rest.find(EVENT_OPEN)) {
        let abs = from + pos;
        let prev = code.get(..abs).and_then(|before| before.chars().last());

        if !prev.is_some_and(is_ident_char) {
            return Some(abs + EVENT_OPEN.len());
        }

        from = abs + EVENT_OPEN.len();
    }

    None
}

fn consume(text: &str, depth: &mut usize, content: &mut String) -> bool {
    for ch in text.chars() {
        match ch {
            '(' => *depth += 1,
            ')' => {
                *depth -= 1;

                if *depth == 0 {
                    return true;
                }
            }
            _ => {}
        }

        if !ch.is_whitespace() {
            content.push(ch);
        }
    }

    false
}

fn has_name_before_level(content: &str) -> bool {
    let Some(name_pos) = content.find("name:") else {
        return false;
    };

    content
        .find("Level::")
        .is_none_or(|level_pos| name_pos < level_pos)
}

fn check_log_named_events(ctx: &FileCtx<'_>) -> Vec<Violation> {
    let mut out = Vec::new();

    for (i, line) in ctx.lines.iter().enumerate() {
        if parse::is_comment(line.trim()) {
            continue;
        }

        let code = scanner::code_only(line);
        let Some(start) = event_invocation_start(&code) else {
            continue;
        };

        let mut content = String::new();
        let mut depth: usize = 1;
        let mut done = consume(code.get(start..).unwrap_or(""), &mut depth, &mut content);
        let mut next = i + 1;

        while !done && next - i < MAX_SCAN_LINES {
            let Some(next_line) = ctx.lines.get(next) else {
                break;
            };

            done = consume(&scanner::code_only(next_line), &mut depth, &mut content);
            next += 1;
        }

        if !has_name_before_level(&content) {
            out.push(violation(
                ctx.rel,
                i + 1,
                "event!() without a leading name: argument — name events `<component>.<operation>.<state>`",
            ));
        }
    }

    out
}

crate::rulewright_test!(check_log_named_events, {
    crate::example_tests!(EXAMPLES, check_log_named_events);
});