about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/crate_in_macro_def.rs125
-rw-r--r--clippy_lints/src/lib.register_all.rs1
-rw-r--r--clippy_lints/src/lib.register_lints.rs1
-rw-r--r--clippy_lints/src/lib.register_suspicious.rs1
-rw-r--r--clippy_lints/src/lib.rs2
-rw-r--r--tests/ui/crate_in_macro_def.fixed56
-rw-r--r--tests/ui/crate_in_macro_def.rs56
-rw-r--r--tests/ui/crate_in_macro_def.stderr10
9 files changed, 253 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index edc29825360..1d1e081e839 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3229,6 +3229,7 @@ Released 2018-09-13
 [`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
 [`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
 [`copy_iterator`]: https://rust-lang.github.io/rust-clippy/master/index.html#copy_iterator
+[`crate_in_macro_def`]: https://rust-lang.github.io/rust-clippy/master/index.html#crate_in_macro_def
 [`create_dir`]: https://rust-lang.github.io/rust-clippy/master/index.html#create_dir
 [`crosspointer_transmute`]: https://rust-lang.github.io/rust-clippy/master/index.html#crosspointer_transmute
 [`dbg_macro`]: https://rust-lang.github.io/rust-clippy/master/index.html#dbg_macro
diff --git a/clippy_lints/src/crate_in_macro_def.rs b/clippy_lints/src/crate_in_macro_def.rs
new file mode 100644
index 00000000000..fc141b4a6e3
--- /dev/null
+++ b/clippy_lints/src/crate_in_macro_def.rs
@@ -0,0 +1,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.61.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
+    }
+}
diff --git a/clippy_lints/src/lib.register_all.rs b/clippy_lints/src/lib.register_all.rs
index 132a4662676..1fb3ca1fd9b 100644
--- a/clippy_lints/src/lib.register_all.rs
+++ b/clippy_lints/src/lib.register_all.rs
@@ -37,6 +37,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
     LintId::of(comparison_chain::COMPARISON_CHAIN),
     LintId::of(copies::IFS_SAME_COND),
     LintId::of(copies::IF_SAME_THEN_ELSE),
+    LintId::of(crate_in_macro_def::CRATE_IN_MACRO_DEF),
     LintId::of(default::FIELD_REASSIGN_WITH_DEFAULT),
     LintId::of(dereference::NEEDLESS_BORROW),
     LintId::of(derivable_impls::DERIVABLE_IMPLS),
diff --git a/clippy_lints/src/lib.register_lints.rs b/clippy_lints/src/lib.register_lints.rs
index 21f1ef562b5..e3161795139 100644
--- a/clippy_lints/src/lib.register_lints.rs
+++ b/clippy_lints/src/lib.register_lints.rs
@@ -97,6 +97,7 @@ store.register_lints(&[
     copies::IF_SAME_THEN_ELSE,
     copies::SAME_FUNCTIONS_IN_IF_CONDITION,
     copy_iterator::COPY_ITERATOR,
+    crate_in_macro_def::CRATE_IN_MACRO_DEF,
     create_dir::CREATE_DIR,
     dbg_macro::DBG_MACRO,
     default::DEFAULT_TRAIT_ACCESS,
diff --git a/clippy_lints/src/lib.register_suspicious.rs b/clippy_lints/src/lib.register_suspicious.rs
index fa3a88e1368..0707e4f8f3d 100644
--- a/clippy_lints/src/lib.register_suspicious.rs
+++ b/clippy_lints/src/lib.register_suspicious.rs
@@ -9,6 +9,7 @@ store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), vec!
     LintId::of(await_holding_invalid::AWAIT_HOLDING_REFCELL_REF),
     LintId::of(casts::CAST_ENUM_CONSTRUCTOR),
     LintId::of(casts::CAST_ENUM_TRUNCATION),
+    LintId::of(crate_in_macro_def::CRATE_IN_MACRO_DEF),
     LintId::of(eval_order_dependence::EVAL_ORDER_DEPENDENCE),
     LintId::of(float_equality_without_abs::FLOAT_EQUALITY_WITHOUT_ABS),
     LintId::of(format_impl::PRINT_IN_FORMAT_IMPL),
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index f2a07999144..c8b57956b1b 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -190,6 +190,7 @@ mod collapsible_match;
 mod comparison_chain;
 mod copies;
 mod copy_iterator;
+mod crate_in_macro_def;
 mod create_dir;
 mod dbg_macro;
 mod default;
@@ -867,6 +868,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
             ignore_publish: cargo_ignore_publish,
         })
     });
+    store.register_early_pass(|| Box::new(crate_in_macro_def::CrateInMacroDef));
     // add lints here, do not remove this comment, it's used in `new_lint`
 }
 
diff --git a/tests/ui/crate_in_macro_def.fixed b/tests/ui/crate_in_macro_def.fixed
new file mode 100644
index 00000000000..9fc594be311
--- /dev/null
+++ b/tests/ui/crate_in_macro_def.fixed
@@ -0,0 +1,56 @@
+// run-rustfix
+#![warn(clippy::crate_in_macro_def)]
+
+mod hygienic {
+    #[macro_export]
+    macro_rules! print_message_hygienic {
+        () => {
+            println!("{}", $crate::hygienic::MESSAGE);
+        };
+    }
+
+    pub const MESSAGE: &str = "Hello!";
+}
+
+mod unhygienic {
+    #[macro_export]
+    macro_rules! print_message_unhygienic {
+        () => {
+            println!("{}", $crate::unhygienic::MESSAGE);
+        };
+    }
+
+    pub const MESSAGE: &str = "Hello!";
+}
+
+mod unhygienic_intentionally {
+    // For cases where the use of `crate` is intentional, applying `allow` to the macro definition
+    // should suppress the lint.
+    #[allow(clippy::crate_in_macro_def)]
+    #[macro_export]
+    macro_rules! print_message_unhygienic_intentionally {
+        () => {
+            println!("{}", crate::CALLER_PROVIDED_MESSAGE);
+        };
+    }
+}
+
+#[macro_use]
+mod not_exported {
+    macro_rules! print_message_not_exported {
+        () => {
+            println!("{}", crate::not_exported::MESSAGE);
+        };
+    }
+
+    pub const MESSAGE: &str = "Hello!";
+}
+
+fn main() {
+    print_message_hygienic!();
+    print_message_unhygienic!();
+    print_message_unhygienic_intentionally!();
+    print_message_not_exported!();
+}
+
+pub const CALLER_PROVIDED_MESSAGE: &str = "Hello!";
diff --git a/tests/ui/crate_in_macro_def.rs b/tests/ui/crate_in_macro_def.rs
new file mode 100644
index 00000000000..ac456108e4a
--- /dev/null
+++ b/tests/ui/crate_in_macro_def.rs
@@ -0,0 +1,56 @@
+// run-rustfix
+#![warn(clippy::crate_in_macro_def)]
+
+mod hygienic {
+    #[macro_export]
+    macro_rules! print_message_hygienic {
+        () => {
+            println!("{}", $crate::hygienic::MESSAGE);
+        };
+    }
+
+    pub const MESSAGE: &str = "Hello!";
+}
+
+mod unhygienic {
+    #[macro_export]
+    macro_rules! print_message_unhygienic {
+        () => {
+            println!("{}", crate::unhygienic::MESSAGE);
+        };
+    }
+
+    pub const MESSAGE: &str = "Hello!";
+}
+
+mod unhygienic_intentionally {
+    // For cases where the use of `crate` is intentional, applying `allow` to the macro definition
+    // should suppress the lint.
+    #[allow(clippy::crate_in_macro_def)]
+    #[macro_export]
+    macro_rules! print_message_unhygienic_intentionally {
+        () => {
+            println!("{}", crate::CALLER_PROVIDED_MESSAGE);
+        };
+    }
+}
+
+#[macro_use]
+mod not_exported {
+    macro_rules! print_message_not_exported {
+        () => {
+            println!("{}", crate::not_exported::MESSAGE);
+        };
+    }
+
+    pub const MESSAGE: &str = "Hello!";
+}
+
+fn main() {
+    print_message_hygienic!();
+    print_message_unhygienic!();
+    print_message_unhygienic_intentionally!();
+    print_message_not_exported!();
+}
+
+pub const CALLER_PROVIDED_MESSAGE: &str = "Hello!";
diff --git a/tests/ui/crate_in_macro_def.stderr b/tests/ui/crate_in_macro_def.stderr
new file mode 100644
index 00000000000..9ac5937dcc0
--- /dev/null
+++ b/tests/ui/crate_in_macro_def.stderr
@@ -0,0 +1,10 @@
+error: `crate` references the macro call's crate
+  --> $DIR/crate_in_macro_def.rs:19:28
+   |
+LL |             println!("{}", crate::unhygienic::MESSAGE);
+   |                            ^^^^^ help: to reference the macro definition's crate, use: `$crate`
+   |
+   = note: `-D clippy::crate-in-macro-def` implied by `-D warnings`
+
+error: aborting due to previous error
+