summaryrefslogtreecommitdiffstats
path: root/src/tools/clippy/clippy_lints/src/crate_in_macro_def.rs
blob: 454ec23388af94511525b335d1e672dc8f6e062a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use clippy_utils::diagnostics::span_lint_and_sugg;
use rustc_ast::ast::{AttrKind, Attribute, Item, ItemKind};
use rustc_ast::token::{Token, TokenKind};
use rustc_ast::tokenstream::{TokenStream, TokenTree};
use rustc_errors::Applicability;
use rustc_lint::{EarlyContext, EarlyLintPass};
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::{symbol::sym, Span};

declare_clippy_lint! {
    /// ### What it does
    /// Checks for use of `crate` as opposed to `$crate` in a macro definition.
    ///
    /// ### Why is this bad?
    /// `crate` refers to the macro call's crate, whereas `$crate` refers to the macro definition's
    /// crate. Rarely is the former intended. See:
    /// https://doc.rust-lang.org/reference/macros-by-example.html#hygiene
    ///
    /// ### Example
    /// ```rust
    /// #[macro_export]
    /// macro_rules! print_message {
    ///     () => {
    ///         println!("{}", crate::MESSAGE);
    ///     };
    /// }
    /// pub const MESSAGE: &str = "Hello!";
    /// ```
    /// Use instead:
    /// ```rust
    /// #[macro_export]
    /// macro_rules! print_message {
    ///     () => {
    ///         println!("{}", $crate::MESSAGE);
    ///     };
    /// }
    /// pub const MESSAGE: &str = "Hello!";
    /// ```
    ///
    /// Note that if the use of `crate` is intentional, an `allow` attribute can be applied to the
    /// macro definition, e.g.:
    /// ```rust,ignore
    /// #[allow(clippy::crate_in_macro_def)]
    /// macro_rules! ok { ... crate::foo ... }
    /// ```
    #[clippy::version = "1.62.0"]
    pub CRATE_IN_MACRO_DEF,
    suspicious,
    "using `crate` in a macro definition"
}
declare_lint_pass!(CrateInMacroDef => [CRATE_IN_MACRO_DEF]);

impl EarlyLintPass for CrateInMacroDef {
    fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
        if_chain! {
            if item.attrs.iter().any(is_macro_export);
            if let ItemKind::MacroDef(macro_def) = &item.kind;
            let tts = macro_def.body.inner_tokens();
            if let Some(span) = contains_unhygienic_crate_reference(&tts);
            then {
                span_lint_and_sugg(
                    cx,
                    CRATE_IN_MACRO_DEF,
                    span,
                    "`crate` references the macro call's crate",
                    "to reference the macro definition's crate, use",
                    String::from("$crate"),
                    Applicability::MachineApplicable,
                );
            }
        }
    }
}

fn is_macro_export(attr: &Attribute) -> bool {
    if_chain! {
        if let AttrKind::Normal(attr_item, _) = &attr.kind;
        if let [segment] = attr_item.path.segments.as_slice();
        then {
            segment.ident.name == sym::macro_export
        } else {
            false
        }
    }
}

fn contains_unhygienic_crate_reference(tts: &TokenStream) -> Option<Span> {
    let mut prev_is_dollar = false;
    let mut cursor = tts.trees();
    while let Some(curr) = cursor.next() {
        if_chain! {
            if !prev_is_dollar;
            if let Some(span) = is_crate_keyword(curr);
            if let Some(next) = cursor.look_ahead(0);
            if is_token(next, &TokenKind::ModSep);
            then {
                return Some(span);
            }
        }
        if let TokenTree::Delimited(_, _, tts) = &curr {
            let span = contains_unhygienic_crate_reference(tts);
            if span.is_some() {
                return span;
            }
        }
        prev_is_dollar = is_token(curr, &TokenKind::Dollar);
    }
    None
}

fn is_crate_keyword(tt: &TokenTree) -> Option<Span> {
    if_chain! {
        if let TokenTree::Token(Token { kind: TokenKind::Ident(symbol, _), span }, _) = tt;
        if symbol.as_str() == "crate";
        then { Some(*span) } else { None }
    }
}

fn is_token(tt: &TokenTree, kind: &TokenKind) -> bool {
    if let TokenTree::Token(Token { kind: other, .. }, _) = tt {
        kind == other
    } else {
        false
    }
}