-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add crate_in_macro_def
lint
#8576
Changes from 2 commits
65a2669
8687205
cb307bb
9b30453
abc221e
75dc406
d6eb82c
aaf04dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
use clippy_utils::diagnostics::span_lint_and_sugg; | ||
use rustc_ast::ast::MacroDef; | ||
use rustc_ast::node_id::NodeId; | ||
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::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_rules! print_message { | ||
/// () => { | ||
/// println!("{}", crate::MESSAGE); | ||
/// }; | ||
/// } | ||
/// pub const MESSAGE: &str = "Hello!"; | ||
/// ``` | ||
/// Use instead: | ||
/// ```rust | ||
/// macro_rules! print_message { | ||
/// () => { | ||
/// println!("{}", $crate::MESSAGE); | ||
/// }; | ||
/// } | ||
/// pub const MESSAGE: &str = "Hello!"; | ||
/// ``` | ||
#[clippy::version = "1.61.0"] | ||
pub CRATE_IN_MACRO_DEF, | ||
correctness, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this should be a correctness lint. Correctness implies the lint level is |
||
"using `crate` in a macro definition" | ||
} | ||
declare_lint_pass!(CrateInMacroDef => [CRATE_IN_MACRO_DEF]); | ||
|
||
impl EarlyLintPass for CrateInMacroDef { | ||
fn check_mac_def(&mut self, cx: &EarlyContext<'_>, macro_def: &MacroDef, _: NodeId) { | ||
let tts = macro_def.body.inner_tokens(); | ||
if let Some(span) = contains_unhygienic_crate_reference(&tts) { | ||
span_lint_and_sugg( | ||
cx, | ||
CRATE_IN_MACRO_DEF, | ||
span, | ||
"reference to the macro call's crate, which is rarely intended", | ||
"if reference to the macro definition's crate is intended, use", | ||
String::from("$crate"), | ||
Applicability::MachineApplicable, | ||
); | ||
} | ||
} | ||
} | ||
|
||
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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// run-rustfix | ||
#![warn(clippy::crate_in_macro_def)] | ||
|
||
#[macro_use] | ||
mod hygienic { | ||
macro_rules! print_message_hygienic { | ||
() => { | ||
println!("{}", $crate::hygienic::MESSAGE); | ||
}; | ||
} | ||
|
||
pub const MESSAGE: &str = "Hello!"; | ||
} | ||
|
||
#[macro_use] | ||
mod unhygienic { | ||
macro_rules! print_message_unhygienic { | ||
() => { | ||
println!("{}", $crate::unhygienic::MESSAGE); | ||
}; | ||
} | ||
|
||
pub const MESSAGE: &str = "Hello!"; | ||
} | ||
|
||
fn main() { | ||
print_message_hygienic!(); | ||
print_message_unhygienic!(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// run-rustfix | ||
#![warn(clippy::crate_in_macro_def)] | ||
|
||
#[macro_use] | ||
mod hygienic { | ||
macro_rules! print_message_hygienic { | ||
() => { | ||
println!("{}", $crate::hygienic::MESSAGE); | ||
}; | ||
} | ||
|
||
pub const MESSAGE: &str = "Hello!"; | ||
} | ||
|
||
#[macro_use] | ||
mod unhygienic { | ||
macro_rules! print_message_unhygienic { | ||
() => { | ||
println!("{}", crate::unhygienic::MESSAGE); | ||
}; | ||
} | ||
|
||
pub const MESSAGE: &str = "Hello!"; | ||
} | ||
|
||
fn main() { | ||
print_message_hygienic!(); | ||
print_message_unhygienic!(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
error: reference to the macro call's crate, which is rarely intended | ||
--> $DIR/crate_in_macro_def.rs:19:28 | ||
| | ||
LL | println!("{}", crate::unhygienic::MESSAGE); | ||
| ^^^^^ | ||
| | ||
= note: `-D clippy::crate-in-macro-def` implied by `-D warnings` | ||
help: if reference to the macro definition's crate is intended, use | ||
| | ||
LL | println!("{}", $crate::unhygienic::MESSAGE); | ||
| ~~~~~~ | ||
|
||
error: aborting due to previous error | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
#![warn(clippy::crate_in_macro_def)] | ||
|
||
#[macro_use] | ||
mod intentional { | ||
// For cases where use of `crate` is intentional, applying `allow` to the macro definition | ||
// should suppress the lint. | ||
#[allow(clippy::crate_in_macro_def)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can go in the same test file as above or be removed, as you aren't having to do something specific to get it working Could be worth noting in the description that the allow attribute has to go on the #[allow(clippy::crate_in_macro_def)]
macro_rules! ok { ... crate::foo ... }
macro_rules! lints {
...
// Still errors since the allow attribute is part of the macro token stream
#[allow(clippy::crate_in_macro_def)]
crate::foo
...
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
My concern is that someone uses |
||
macro_rules! print_message { | ||
() => { | ||
println!("{}", crate::CALLER_PROVIDED_MESSAGE); | ||
}; | ||
} | ||
} | ||
|
||
fn main() { | ||
print_message!(); | ||
} | ||
|
||
pub const CALLER_PROVIDED_MESSAGE: &str = "Hello!"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For internal macros
crate
is fine, you could check for amacro_export
attribute usingcheck_item
to avoid some false positivesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am embarrassed that I didn't think of this. 😬