Skip to main content

wowlab_tidy/languages/rust/rules/macros/
macro_hidden_items.rs

1use 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});