wowlab_tidy/languages/rust/rules/macros/
proc_macro_thin_shim.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self, HasArgList, HasAttrs, HasName},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "thin function-like shim",
12 code: "#[proc_macro]\npub fn my_macro(input: TokenStream) -> TokenStream {\n my_macro_impl::my_macro(input.into()).into()\n}",
13 pass: true,
14 },
15 Example {
16 label: "thin attribute shim with two args",
17 code: "#[proc_macro_attribute]\npub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {\n route_impl::route(attr.into(), item.into()).into()\n}",
18 pass: true,
19 },
20 Example {
21 label: "thin derive shim",
22 code: "#[proc_macro_derive(Model, attributes(model))]\npub fn derive_model(input: TokenStream) -> TokenStream {\n model_impl::derive_model(input.into()).into()\n}",
23 pass: true,
24 },
25 Example {
26 label: "inline expansion logic",
27 code: "#[proc_macro]\npub fn my_macro(input: TokenStream) -> TokenStream {\n let text = input.to_string();\n text.parse().unwrap_or_default()\n}",
28 pass: false,
29 },
30 Example {
31 label: "delegation without impl crate",
32 code: "#[proc_macro]\npub fn my_macro(input: TokenStream) -> TokenStream {\n expand(input)\n}",
33 pass: false,
34 },
35 Example {
36 label: "plain fn with any body",
37 code: "fn helper(input: String) -> String { input }",
38 pass: true,
39 },
40];
41
42crate::ast_rule!(
43 proc_macro_thin_shim,
44 "Require proc-macro entry points to be thin `impl_crate::name(arg.into()).into()` shims.",
45 "Token-stream logic living behind #[proc_macro] cannot be unit- or snapshot-tested — it belongs in a separate impl crate (M-PROC-IMPL).",
46 Low,
47);
48
49const PROC_MACRO_ATTRS: &[&str] = &["proc_macro", "proc_macro_derive", "proc_macro_attribute"];
50const MIN_DELEGATE_SEGMENTS: usize = 2;
51
52fn check_proc_macro_thin_shim(ctx: &AstCtx<'_>) -> Vec<Violation> {
53 ctx.nodes::<ast::Fn>()
54 .filter(|function| {
55 !ctx.is_in_test(function) && is_proc_macro_fn(function) && !is_thin_shim(function)
56 })
57 .map(|function| {
58 let name = function
59 .name()
60 .map_or_else(String::new, |name| name.text().to_string());
61
62 ctx.violation(
63 &function,
64 format!(
65 "proc macro `{name}` is not a thin shim — delegate to a separate impl \
66 crate: `foo_impl::{name}(input.into()).into()` (M-PROC-IMPL)"
67 ),
68 )
69 })
70 .collect()
71}
72
73fn is_proc_macro_fn(function: &ast::Fn) -> bool {
74 function.attrs().any(|attr| {
75 attr.simple_name()
76 .is_some_and(|name| PROC_MACRO_ATTRS.contains(&name.as_str()))
77 })
78}
79
80fn is_thin_shim(function: &ast::Fn) -> bool {
81 let Some(statements) = function.body().and_then(|body| body.stmt_list()) else {
82 return false;
83 };
84
85 if statements.statements().next().is_some() {
86 return false;
87 }
88
89 let Some(ast::Expr::MethodCallExpr(into_call)) = statements.tail_expr() else {
90 return false;
91 };
92
93 if into_call
94 .name_ref()
95 .is_none_or(|name| name.text() != "into")
96 || into_call
97 .arg_list()
98 .is_some_and(|args| args.args().next().is_some())
99 {
100 return false;
101 }
102
103 let Some(ast::Expr::CallExpr(delegate)) = into_call.receiver() else {
104 return false;
105 };
106 let Some(ast::Expr::PathExpr(path_expr)) = delegate.expr() else {
107 return false;
108 };
109
110 if path_expr
111 .path()
112 .is_none_or(|path| path.syntax().to_string().split("::").count() < MIN_DELEGATE_SEGMENTS)
113 {
114 return false;
115 }
116
117 delegate.arg_list().is_some_and(|arguments| {
118 arguments
119 .args()
120 .all(|argument| is_into_adapted_arg(&argument))
121 })
122}
123
124fn is_into_adapted_arg(arg: &ast::Expr) -> bool {
125 match arg {
126 ast::Expr::PathExpr(_) => true,
127 ast::Expr::MethodCallExpr(call) => {
128 call.name_ref().is_some_and(|name| name.text() == "into")
129 && call
130 .arg_list()
131 .is_none_or(|args| args.args().next().is_none())
132 && matches!(call.receiver(), Some(ast::Expr::PathExpr(_)))
133 }
134 _ => false,
135 }
136}
137
138crate::tidy_ast_test!(check_proc_macro_thin_shim, {
139 crate::example_tests!(EXAMPLES, check_proc_macro_thin_shim);
140});