wowlab_tidy/languages/rust/rules/macros/
macro_hidden_items.rs1use line_index::LineIndex;
2use ra_ap_syntax::{
3 AstNode, Edition, SourceFile, SyntaxKind,
4 ast::{self, HasName, HasVisibility, VisibilityKind},
5};
6
7use super::support::is_quote_call;
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12 Example {
13 label: "fixed-name pub struct in quote body",
14 code: "fn expand() {\n let _ = quote::quote! { pub struct Generated; };\n}",
15 pass: false,
16 },
17 Example {
18 label: "fixed-name pub fn in quote body",
19 code: "fn expand() {\n let _ = quote::quote! { pub fn helper() {} };\n}",
20 pass: false,
21 },
22 Example {
23 label: "fixed-name pub enum in quote body",
24 code: "fn expand() {\n let _ = quote::quote! { pub enum Kind {} };\n}",
25 pass: false,
26 },
27 Example {
28 label: "fixed-name pub trait in quote body",
29 code: "fn expand() {\n let _ = quote::quote! { pub trait Ext {} };\n}",
30 pass: false,
31 },
32 Example {
33 label: "interpolated ident passes",
34 code: "fn expand() {\n let _ = quote::quote! { pub struct #name; };\n}",
35 pass: true,
36 },
37 Example {
38 label: "repeated interpolation passes",
39 code: "fn expand() {\n let _ = quote::quote! { #(pub fn #getters();)* };\n}",
40 pass: true,
41 },
42 Example {
43 label: "private emitted item passes",
44 code: "fn expand() {\n let _ = quote::quote! { struct Hidden; };\n}",
45 pass: true,
46 },
47 Example {
48 label: "pub item outside quote body",
49 code: "pub struct Normal;",
50 pass: true,
51 },
52 Example {
53 label: "opener in comment",
54 code: "// quote! { pub struct Generated; }",
55 pass: true,
56 },
57];
58
59crate::ast_rule!(
60 macro_hidden_items,
61 "Flag fixed-name `pub` items emitted from quote! bodies.",
62 "Fixed-name emitted items collide across expansions and clash with user code — interpolate the identifier instead (M-PROC-IMPLIED-ITEMS).",
63 Medium,
64);
65
66fn check_macro_hidden_items(ctx: &AstCtx<'_>) -> Vec<Violation> {
67 let quote_trees = ctx
68 .nodes::<ast::MacroCall>()
69 .filter(|call| !ctx.is_in_test(call) && is_quote_call(call))
70 .filter_map(|call| call.token_tree());
71
72 quote_trees
73 .flat_map(|tree| emitted_public_items(ctx, &tree))
74 .collect()
75}
76
77fn emitted_public_items(ctx: &AstCtx<'_>, tree: &ast::TokenTree) -> Vec<Violation> {
78 let Some((source, body_line)) = token_tree_body(ctx, tree) else {
79 return Vec::new();
80 };
81 let parse = SourceFile::parse(&source, Edition::Edition2024);
82 let root = parse.tree();
83 let line_index = LineIndex::new(&source);
84
85 root.syntax()
86 .descendants()
87 .filter_map(public_item)
88 .map(|(item, ident)| {
89 let relative_line = line_index.line_col(item.text_range().start()).line as usize;
90
91 crate::violation(
92 ctx.file.rel,
93 body_line + relative_line,
94 format!(
95 "macro emits fixed-name public item `{ident}` — fixed names collide \
96 across expansions, interpolate the identifier (M-PROC-IMPLIED-ITEMS)"
97 ),
98 )
99 })
100 .collect()
101}
102
103fn public_item(node: ra_ap_syntax::SyntaxNode) -> Option<(ra_ap_syntax::SyntaxNode, String)> {
104 if !matches!(
105 node.kind(),
106 SyntaxKind::STRUCT | SyntaxKind::ENUM | SyntaxKind::TRAIT | SyntaxKind::FN
107 ) {
108 return None;
109 }
110
111 let visibility = ast::AnyHasVisibility::cast(node.clone())?.visibility()?;
112
113 if !matches!(visibility.kind(), VisibilityKind::Pub) {
114 return None;
115 }
116
117 let name = ast::AnyHasName::cast(node.clone())?
118 .name()?
119 .text()
120 .to_string();
121
122 Some((node, name))
123}
124
125fn token_tree_body(ctx: &AstCtx<'_>, tree: &ast::TokenTree) -> Option<(String, usize)> {
126 let opener = tree
127 .l_paren_token()
128 .or_else(|| tree.l_brack_token())
129 .or_else(|| tree.l_curly_token())?;
130 let closer = tree
131 .r_paren_token()
132 .or_else(|| tree.r_brack_token())
133 .or_else(|| tree.r_curly_token())?;
134 let start = u32::from(opener.text_range().end()) as usize;
135 let end = u32::from(closer.text_range().start()) as usize;
136 let source = ctx.file.contents.get(start..end).map(str::to_owned)?;
137 let body_line = ctx.line_index.line_col(opener.text_range().start()).line as usize + 1;
138
139 Some((source, body_line))
140}
141
142crate::tidy_ast_test!(check_macro_hidden_items, {
143 crate::example_tests!(EXAMPLES, check_macro_hidden_items);
144});