Skip to main content

wowlab_tidy/languages/rust/rules/macros/
proc_macro_justification.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: "proc macro without justification",
11        code: "#[proc_macro]\npub fn my_macro(input: TokenStream) -> TokenStream {\n    expand(input)\n}",
12        pass: false,
13    },
14    Example {
15        label: "derive with justification",
16        code: "// WHY-PROC: derive needs field introspection impossible in macro_rules\n#[proc_macro_derive(Model)]\npub fn derive_model(input: TokenStream) -> TokenStream {\n    expand(input)\n}",
17        pass: true,
18    },
19    Example {
20        label: "justification above other attributes",
21        code: "// WHY-PROC: attribute must rewrite the item's signature\n#[doc(hidden)]\n#[proc_macro_attribute]\npub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {\n    item\n}",
22        pass: true,
23    },
24    Example {
25        label: "justification separated by blank line",
26        code: "// WHY-PROC: needs token-level inspection\n\n#[proc_macro]\npub fn my_macro(input: TokenStream) -> TokenStream {\n    expand(input)\n}",
27        pass: true,
28    },
29    Example {
30        label: "justification too far above",
31        code: "// WHY-PROC: reason\n// filler one\n// filler two\n// filler three\n#[proc_macro]\npub fn my_macro(input: TokenStream) -> TokenStream {\n    expand(input)\n}",
32        pass: false,
33    },
34    Example {
35        label: "code between justification and attribute",
36        code: "// WHY-PROC: reason\nfn other() {}\n#[proc_macro]\npub fn my_macro(input: TokenStream) -> TokenStream {\n    expand(input)\n}",
37        pass: false,
38    },
39    Example {
40        label: "attribute in string literal",
41        code: "let s = \"#[proc_macro]\";",
42        pass: true,
43    },
44];
45
46crate::line_rule!(
47    proc_macro_justification,
48    "Require a `// WHY-PROC:` comment above every proc-macro attribute.",
49    "Proc macros are a last resort — each one must record why a macro-by-example cannot do the job (M-EXAMPLE-OVER-PROC, M-MACRO-LAST-RESORT).",
50    Medium,
51);
52
53const JUSTIFICATION_TAG: &str = "WHY-PROC:";
54const LOOKBACK_LINES: usize = 3;
55
56fn check_proc_macro_justification(ctx: &FileCtx<'_>) -> Vec<Violation> {
57    let mut out = Vec::new();
58
59    for (index, line) in ctx.lines.iter().enumerate() {
60        let code = scanner::code_only(line);
61
62        if !is_proc_macro_attr(&code) {
63            continue;
64        }
65
66        if has_justification(ctx.lines, index) {
67            continue;
68        }
69
70        out.push(violation(
71            ctx.rel,
72            index + 1,
73            "proc-macro attribute without a preceding `// WHY-PROC:` comment explaining \
74             why a macro-by-example cannot do the job (M-EXAMPLE-OVER-PROC)",
75        ));
76    }
77
78    out
79}
80
81fn is_proc_macro_attr(code: &str) -> bool {
82    let trimmed = code.trim();
83
84    trimmed == "#[proc_macro]"
85        || trimmed == "#[proc_macro_attribute]"
86        || trimmed == "#[proc_macro_derive]"
87        || trimmed.starts_with("#[proc_macro_derive(")
88}
89
90fn has_justification(lines: &[&str], attr_index: usize) -> bool {
91    let mut budget = LOOKBACK_LINES;
92    let mut index = attr_index;
93
94    while index > 0 && budget > 0 {
95        index -= 1;
96        let Some(trimmed) = lines.get(index).map(|raw| raw.trim()) else {
97            break;
98        };
99
100        if trimmed.starts_with("#[") || trimmed.is_empty() {
101            continue;
102        }
103
104        if !parse::is_comment(trimmed) {
105            break;
106        }
107
108        if trimmed.contains(JUSTIFICATION_TAG) {
109            return true;
110        }
111
112        budget -= 1;
113    }
114
115    false
116}
117
118crate::tidy_test!(check_proc_macro_justification, {
119    crate::example_tests!(EXAMPLES, check_proc_macro_justification);
120});