wowlab_tidy/languages/rust/rules/macros/
proc_macro_justification.rs1use 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});