Skip to content

Commit cc5ab4a

Browse files
committed
add new lint [unsafe_block_in_proc_macro]
that detects unsafe block in `quote!` Signed-off-by: J-ZhengLi <[email protected]>
1 parent c091b77 commit cc5ab4a

File tree

6 files changed

+436
-1
lines changed

6 files changed

+436
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4636,7 +4636,6 @@ Released 2018-09-13
46364636
[`large_enum_variant`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
46374637
[`large_futures`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_futures
46384638
[`large_include_file`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_include_file
4639-
[`large_memory_allocation`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_memory_allocation
46404639
[`large_stack_arrays`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_stack_arrays
46414640
[`large_types_passed_by_value`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_types_passed_by_value
46424641
[`len_without_is_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#len_without_is_empty
@@ -5007,6 +5006,7 @@ Released 2018-09-13
50075006
[`unnested_or_patterns`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnested_or_patterns
50085007
[`unreachable`]: https://rust-lang.github.io/rust-clippy/master/index.html#unreachable
50095008
[`unreadable_literal`]: https://rust-lang.github.io/rust-clippy/master/index.html#unreadable_literal
5009+
[`unsafe_block_in_proc_macro`]: https://rust-lang.github.io/rust-clippy/master/index.html#unsafe_block_in_proc_macro
50105010
[`unsafe_derive_deserialize`]: https://rust-lang.github.io/rust-clippy/master/index.html#unsafe_derive_deserialize
50115011
[`unsafe_removed_from_name`]: https://rust-lang.github.io/rust-clippy/master/index.html#unsafe_removed_from_name
50125012
[`unsafe_vector_initialization`]: https://rust-lang.github.io/rust-clippy/master/index.html#unsafe_vector_initialization

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
632632
crate::unnecessary_struct_initialization::UNNECESSARY_STRUCT_INITIALIZATION_INFO,
633633
crate::unnecessary_wraps::UNNECESSARY_WRAPS_INFO,
634634
crate::unnested_or_patterns::UNNESTED_OR_PATTERNS_INFO,
635+
crate::unsafe_block_in_proc_macro::UNSAFE_BLOCK_IN_PROC_MACRO_INFO,
635636
crate::unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME_INFO,
636637
crate::unused_async::UNUSED_ASYNC_INFO,
637638
crate::unused_io_amount::UNUSED_IO_AMOUNT_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ mod unnecessary_self_imports;
310310
mod unnecessary_struct_initialization;
311311
mod unnecessary_wraps;
312312
mod unnested_or_patterns;
313+
mod unsafe_block_in_proc_macro;
313314
mod unsafe_removed_from_name;
314315
mod unused_async;
315316
mod unused_io_amount;
@@ -962,6 +963,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
962963
store.register_early_pass(|| Box::new(suspicious_doc_comments::SuspiciousDocComments));
963964
let mem_unsafe_functions = conf.mem_unsafe_functions.clone();
964965
store.register_late_pass(move |_| Box::new(guidelines::GuidelineLints::new(mem_unsafe_functions.clone())));
966+
store.register_late_pass(|_| Box::new(unsafe_block_in_proc_macro::UnsafeBlockInProcMacro::new()));
965967
// add lints here, do not remove this comment, it's used in `new_lint`
966968
}
967969

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use clippy_utils::diagnostics::span_lint_and_help;
2+
use clippy_utils::source::snippet_opt;
3+
use if_chain::if_chain;
4+
use rustc_data_structures::fx::FxHashSet;
5+
use rustc_hir::Expr;
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_session::{declare_tool_lint, impl_lint_pass};
8+
use rustc_span::hygiene::{ExpnKind, MacroKind};
9+
use rustc_span::{sym, Span};
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
/// Checks for unsafe block written in procedural macro
14+
///
15+
/// ### Why is this bad?
16+
/// It hides the unsafe code, making the safety of expended code unsound.
17+
///
18+
/// ### Known problems
19+
/// Possible FP when the user uses proc-macro to generate a function with unsafe block in it.
20+
///
21+
/// ### Example
22+
/// ```rust
23+
/// #[proc_macro]
24+
/// pub fn rprintf(input: TokenStream) -> TokenStream {
25+
/// let expr = parse_macro_input!(input as syn::Expr);
26+
/// quote!({
27+
/// unsafe {
28+
/// // unsafe operation
29+
/// }
30+
/// })
31+
/// }
32+
///
33+
/// // This allows users to use this macro without `unsafe` block
34+
/// rprintf!();
35+
/// ```
36+
/// Use instead:
37+
/// ```rust
38+
/// #[proc_macro]
39+
/// pub fn rprintf(input: TokenStream) -> TokenStream {
40+
/// let expr = parse_macro_input!(input as syn::Expr);
41+
/// quote!({
42+
/// // unsafe operation
43+
/// })
44+
/// }
45+
///
46+
/// // When using this macro, an outer `unsafe` block is needed,
47+
/// // making the safety of this macro much clearer.
48+
/// unsafe { rprintf!(); }
49+
/// ```
50+
#[clippy::version = "1.70.0"]
51+
pub UNSAFE_BLOCK_IN_PROC_MACRO,
52+
nursery,
53+
"using unsafe block in procedural macro's definition"
54+
}
55+
56+
#[derive(Clone)]
57+
pub struct UnsafeBlockInProcMacro {
58+
call_sites: FxHashSet<Span>,
59+
}
60+
61+
impl UnsafeBlockInProcMacro {
62+
pub fn new() -> Self {
63+
Self {
64+
call_sites: FxHashSet::default(),
65+
}
66+
}
67+
}
68+
69+
impl_lint_pass!(UnsafeBlockInProcMacro => [UNSAFE_BLOCK_IN_PROC_MACRO]);
70+
71+
impl LateLintPass<'_> for UnsafeBlockInProcMacro {
72+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
73+
let expn_data = expr.span.ctxt().outer_expn_data();
74+
let call_site = expn_data.call_site;
75+
76+
if_chain! {
77+
if !call_site.from_expansion();
78+
if self.call_sites.insert(call_site);
79+
if let ExpnKind::Macro(MacroKind::Bang, symbol) = expn_data.kind;
80+
if symbol == sym::quote;
81+
if let Some(code_snip) = quote_inner(cx, call_site);
82+
let could_be_fn = code_snip.contains("fn ");
83+
if contains_unsafe_block(&code_snip);
84+
then {
85+
emit_lint_message(cx, expr, call_site, could_be_fn);
86+
}
87+
}
88+
}
89+
}
90+
91+
/// Get the code snippet inside of `quote!()`
92+
fn quote_inner(cx: &LateContext<'_>, call_site: Span) -> Option<String> {
93+
let quote_snip = snippet_opt(cx, call_site)?;
94+
let (_, snip) = quote_snip.split_once('!')?;
95+
let mut chars = snip.trim().chars();
96+
chars.next();
97+
chars.next_back();
98+
Some(chars.collect())
99+
}
100+
101+
fn contains_unsafe_block(code: &str) -> bool {
102+
// unify input by eliminating all whitespaces
103+
let without_ws: String = code.split_whitespace().collect();
104+
without_ws.contains("unsafe{")
105+
}
106+
107+
fn emit_lint_message(cx: &LateContext<'_>, expr: &Expr<'_>, call_site: Span, could_be_fn: bool) {
108+
let parent_hid = cx.tcx.hir().parent_id(expr.hir_id);
109+
let name = cx.tcx.item_name(parent_hid.owner.to_def_id());
110+
let msg = &format!("procedural macro `{name}` may hiding unsafe operations");
111+
let extra_sugg = if could_be_fn {
112+
"if the block was in a function, \
113+
put an `unsafe` keyword on its definition: `unsafe fn ...`, \n\
114+
otherwise"
115+
} else {
116+
"and"
117+
};
118+
let help = &format!(
119+
"consider removing unsafe block from the above `quote!` call,\n\
120+
{extra_sugg} add unsafe block when calling the macro instead: `unsafe {{ {name}!{{...}} }}`"
121+
);
122+
123+
span_lint_and_help(cx, UNSAFE_BLOCK_IN_PROC_MACRO, call_site, msg, None, help);
124+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#![warn(clippy::unsafe_block_in_proc_macro)]
2+
#![allow(clippy::needless_return)]
3+
4+
extern crate proc_macro;
5+
extern crate quote;
6+
7+
use proc_macro::TokenStream;
8+
use quote::quote;
9+
10+
fn do_something() {}
11+
fn main() {}
12+
13+
// Ideally we can add `#[proc_macro]` attr on these following functions,
14+
// and check only such functions instead, but I don't know how to write such test
15+
// without putting `proc_macro = true` in Cargo.toml, maybe someone else can fix it.
16+
17+
pub fn unsafe_print_foo(_: TokenStream) -> TokenStream {
18+
quote!({
19+
unsafe {
20+
println!("foo");
21+
}
22+
})
23+
.into()
24+
}
25+
26+
#[rustfmt::skip]
27+
pub fn unsafe_print_bar_unformatted(_: TokenStream) -> TokenStream {
28+
quote!({
29+
unsafe
30+
31+
32+
{ println!("bar"); }
33+
}).into()
34+
}
35+
36+
#[rustfmt::skip]
37+
pub fn unsafe_print_bar_unformatted_1(_: TokenStream) -> TokenStream {
38+
quote!({
39+
unsafe{println!("bar");}
40+
}).into()
41+
}
42+
43+
pub fn unsafe_print_baz(_: TokenStream) -> TokenStream {
44+
do_something();
45+
46+
quote!({
47+
let _ = 0_u8;
48+
do_something();
49+
unsafe {
50+
println!("baz");
51+
}
52+
53+
do_something();
54+
})
55+
.into()
56+
}
57+
58+
pub fn maybe_unsafe_print(_: TokenStream) -> TokenStream {
59+
do_something();
60+
61+
if false {
62+
return quote!({
63+
unsafe {
64+
println!("unsafe");
65+
}
66+
})
67+
.into();
68+
}
69+
70+
quote!({
71+
println!("safe");
72+
})
73+
.into()
74+
}
75+
76+
pub fn maybe_unsafe_print_1(_: TokenStream) -> TokenStream {
77+
let condition = 1;
78+
if condition > 0 {
79+
return quote!({
80+
println!("safe");
81+
})
82+
.into();
83+
}
84+
85+
quote!({
86+
unsafe {
87+
println!("unsafe");
88+
}
89+
})
90+
.into()
91+
}
92+
93+
pub fn maybe_unsafe_print_2(_: TokenStream) -> TokenStream {
94+
let condition = 1;
95+
if condition == 0 {
96+
return quote!({}).into();
97+
} else if condition == 1 {
98+
return quote!(unsafe {}).into();
99+
} else if condition == 2 {
100+
return quote!({ println!("2") }).into();
101+
}
102+
do_something();
103+
quote!({}).into()
104+
}
105+
106+
pub fn multiple_unsafe(_: TokenStream) -> TokenStream {
107+
let condition = 1;
108+
match condition {
109+
1 => quote!(unsafe {
110+
println!("1");
111+
}),
112+
2 => quote!(unsafe {
113+
println!("2");
114+
}),
115+
_ => quote!(unsafe {}),
116+
}
117+
.into()
118+
}
119+
120+
pub fn unsafe_block_in_function(_: TokenStream) -> TokenStream {
121+
quote!({
122+
fn print_foo() {
123+
unsafe {
124+
println!("foo");
125+
}
126+
}
127+
})
128+
.into()
129+
}
130+
131+
// Don't lint
132+
pub fn print_foo(_: TokenStream) -> TokenStream {
133+
quote!({
134+
println!("foo");
135+
})
136+
.into()
137+
}
138+
139+
// Don't lint
140+
pub fn unsafe_trait(_: TokenStream) -> TokenStream {
141+
quote!({
142+
unsafe trait Foo {
143+
fn aaa();
144+
fn bbb();
145+
}
146+
})
147+
.into()
148+
}
149+
150+
// Don't lint
151+
pub fn unsafe_fn(_: TokenStream) -> TokenStream {
152+
quote!({
153+
unsafe fn uns_foo() {}
154+
})
155+
.into()
156+
}
157+
158+
// Don't lint
159+
pub fn unsafe_impl_fn(_: TokenStream) -> TokenStream {
160+
quote!({
161+
struct T;
162+
impl T {
163+
unsafe fn foo() {}
164+
}
165+
})
166+
.into()
167+
}

0 commit comments

Comments
 (0)