Skip to content
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

undocumented_unsafe_blocks does not trigger on unsafe trait impls #8761

Merged
merged 5 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 138 additions & 28 deletions clippy_lints/src/undocumented_unsafe_blocks.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use clippy_utils::diagnostics::span_lint_and_help;
use clippy_utils::is_lint_allowed;
use clippy_utils::source::walk_span_to_context;
use clippy_utils::{get_parent_node, is_lint_allowed};
use rustc_data_structures::sync::Lrc;
use rustc_hir::{Block, BlockCheckMode, UnsafeSource};
use rustc_hir as hir;
use rustc_hir::{Block, BlockCheckMode, ItemKind, Node, UnsafeSource};
use rustc_lexer::{tokenize, TokenKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::lint::in_external_macro;
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::{BytePos, Pos, SyntaxContext};
use rustc_span::{BytePos, Pos, Span, SyntaxContext};

declare_clippy_lint! {
/// ### What it does
/// Checks for `unsafe` blocks without a `// SAFETY: ` comment
/// Checks for `unsafe` blocks and impls without a `// SAFETY: ` comment
/// explaining why the unsafe operations performed inside
/// the block are safe.
///
Expand All @@ -34,7 +35,7 @@ declare_clippy_lint! {
/// ```
///
/// ### Why is this bad?
/// Undocumented unsafe blocks can make it difficult to
/// Undocumented unsafe blocks and impls can make it difficult to
/// read and maintain code, as well as uncover unsoundness
/// and bugs.
///
Expand Down Expand Up @@ -66,7 +67,7 @@ impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
&& !in_external_macro(cx.tcx.sess, block.span)
&& !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id)
&& !is_unsafe_from_proc_macro(cx, block)
&& !is_unsafe_from_proc_macro(cx, block.span)
&& !block_has_safety_comment(cx, block)
{
let source_map = cx.tcx.sess.source_map();
Expand All @@ -86,11 +87,37 @@ impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
);
}
}

fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) {
if let hir::ItemKind::Impl(imple) = item.kind
&& imple.unsafety == hir::Unsafety::Unsafe
&& !in_external_macro(cx.tcx.sess, item.span)
&& !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id())
&& !is_unsafe_from_proc_macro(cx, item.span)
&& !item_has_safety_comment(cx, item)
{
let source_map = cx.tcx.sess.source_map();
let span = if source_map.is_multiline(item.span) {
source_map.span_until_char(item.span, '\n')
} else {
item.span
};

span_lint_and_help(
cx,
UNDOCUMENTED_UNSAFE_BLOCKS,
span,
"unsafe impl missing a safety comment",
None,
"consider adding a safety comment on the preceding line",
);
}
}
}

fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool {
let source_map = cx.sess().source_map();
let file_pos = source_map.lookup_byte_offset(block.span.lo());
let file_pos = source_map.lookup_byte_offset(span.lo());
file_pos
.sf
.src
Expand All @@ -100,7 +127,7 @@ fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
}

/// Checks if the lines immediately preceding the block contain a safety comment.
fn block_has_safety_comment(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
fn block_has_safety_comment(cx: &LateContext<'_>, block: &hir::Block<'_>) -> bool {
// This intentionally ignores text before the start of a function so something like:
// ```
// // SAFETY: reason
Expand All @@ -109,13 +136,85 @@ fn block_has_safety_comment(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
// won't work. This is to avoid dealing with where such a comment should be place relative to
// attributes and doc comments.

span_from_macro_expansion_has_safety_comment(cx, block.span) || span_in_body_has_safety_comment(cx, block.span)
}

/// Checks if the lines immediately preceding the item contain a safety comment.
fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> bool {
if span_from_macro_expansion_has_safety_comment(cx, item.span) || span_in_body_has_safety_comment(cx, item.span) {
Copy link
Contributor

@Jarcho Jarcho May 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be returning false if the item is within a function, but doesn't have a safety comment. e.g.

unsafe trait Foo {}
fn f() {
    Struct S;
    // no comment
    unsafe impl Foo for S {}
}

Not a particularly big deal on it's own, but it contributes to the ICE later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still duplicate work here. Easiest fix would be to remove the call to span_in_body_has_safety_comment as it's handled afterwards when matching on Node::Stmt(_).

return true;
}

if item.span.ctxt() == SyntaxContext::root() {
if let Some(parent_node) = get_parent_node(cx.tcx, item.hir_id()) {
let mut span_before_item = None;
let mut hi = false;
if let Node::Item(parent_item) = parent_node {
if let ItemKind::Mod(parent_mod) = &parent_item.kind {
for (idx, item_id) in parent_mod.item_ids.iter().enumerate() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use Iterator::find_map here. the extraction of either Span::lo or Span::hi could be done here as well.

This should get rid of the need to have span_before_item and hi as mutable.

if *item_id == item.item_id() {
if idx == 0 {
// mod A { /* comment */ unsafe impl T {} ... }
// ^------------------------------------------^ gets this span
// ^---------------------^ finally checks the text in this range
hi = false;
span_before_item = Some(parent_item.span);
} else {
let prev_item = cx.tcx.hir().item(parent_mod.item_ids[idx - 1]);
// some_item /* comment */ unsafe impl T {}
// ^-------^ gets this span
// ^---------------^ finally checks the text in this range
hi = true;
span_before_item = Some(prev_item.span);
}
break;
}
}
}
}
let span_before_item = span_before_item.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can fail when the impl is inside a function and I think the crate root.


let source_map = cx.sess().source_map();
if let Some(item_span) = walk_span_to_context(item.span, SyntaxContext::root())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

walk_span_to_context isn't needed here. item.span is already at the root context.

&& let Some(span_before_item) = walk_span_to_context(span_before_item, SyntaxContext::root())
&& let Ok(unsafe_line) = source_map.lookup_line(item_span.lo())
&& let Ok(line_before_unsafe) = source_map.lookup_line(if hi {
span_before_item.hi()
} else {
span_before_item.lo()
})
&& Lrc::ptr_eq(&unsafe_line.sf, &line_before_unsafe.sf)
&& let Some(src) = unsafe_line.sf.src.as_deref()
{
line_before_unsafe.line < unsafe_line.line && text_has_safety_comment(
src,
&unsafe_line.sf.lines[line_before_unsafe.line + 1..=unsafe_line.line],
unsafe_line.sf.start_pos.to_usize(),
)
} else {
// Problem getting source text. Pretend a comment was found.
true
}
} else {
// No parent node. Pretend a comment was found.
true
}
} else {
false
}
}

fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
let source_map = cx.sess().source_map();
let ctxt = block.span.ctxt();
if ctxt != SyntaxContext::root() {
// From a macro expansion. Get the text from the start of the macro declaration to start of the unsafe block.
let ctxt = span.ctxt();
if ctxt == SyntaxContext::root() {
false
} else {
// From a macro expansion. Get the text from the start of the macro declaration to start of the
// unsafe block.
// macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
// ^--------------------------------------------^
if let Ok(unsafe_line) = source_map.lookup_line(block.span.lo())
if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
&& let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
&& Lrc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
&& let Some(src) = unsafe_line.sf.src.as_deref()
Expand All @@ -129,24 +228,35 @@ fn block_has_safety_comment(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
// Problem getting source text. Pretend a comment was found.
true
}
} else if let Ok(unsafe_line) = source_map.lookup_line(block.span.lo())
}
}

fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
let source_map = cx.sess().source_map();
let ctxt = span.ctxt();
if ctxt == SyntaxContext::root()
&& let Some(body) = cx.enclosing_body
&& let Some(body_span) = walk_span_to_context(cx.tcx.hir().body(body).value.span, SyntaxContext::root())
&& let Ok(body_line) = source_map.lookup_line(body_span.lo())
&& Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
&& let Some(src) = unsafe_line.sf.src.as_deref()
{
// Get the text from the start of function body to the unsafe block.
// fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
// ^-------------^
body_line.line < unsafe_line.line && text_has_safety_comment(
src,
&unsafe_line.sf.lines[body_line.line + 1..=unsafe_line.line],
unsafe_line.sf.start_pos.to_usize(),
)
if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
&& let Some(body_span) = walk_span_to_context(cx.tcx.hir().body(body).value.span, SyntaxContext::root())
&& let Ok(body_line) = source_map.lookup_line(body_span.lo())
&& Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
&& let Some(src) = unsafe_line.sf.src.as_deref()
{
// Get the text from the start of function body to the unsafe block.
// fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
// ^-------------^
body_line.line < unsafe_line.line && text_has_safety_comment(
src,
&unsafe_line.sf.lines[body_line.line + 1..=unsafe_line.line],
unsafe_line.sf.start_pos.to_usize(),
)
} else {
// Problem getting source text. Pretend a comment was found.
true
}
} else {
// Problem getting source text. Pretend a comment was found.
true
false
}
}

Expand Down
108 changes: 107 additions & 1 deletion tests/ui/undocumented_unsafe_blocks.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// aux-build:proc_macro_unsafe.rs

#![warn(clippy::undocumented_unsafe_blocks)]
#![allow(clippy::let_unit_value)]
#![allow(clippy::let_unit_value, clippy::missing_safety_doc)]

extern crate proc_macro_unsafe;

Expand Down Expand Up @@ -334,4 +334,110 @@ pub fn print_binary_tree() {
println!("{}", unsafe { String::from_utf8_unchecked(vec![]) });
}

mod unsafe_impl_smoke_test {
unsafe trait A {}

// error: no safety comment
unsafe impl A for () {}

// Safety: ok
unsafe impl A for (i32) {}

mod sub_mod {
// error: also works for the first item
unsafe impl B for (u32) {}
unsafe trait B {}
}

#[rustfmt::skip]
mod sub_mod2 {
//
// SAFETY: ok
//

unsafe impl B for (u32) {}
unsafe trait B {}
}
}

mod unsafe_impl_from_macro {
unsafe trait T {}

macro_rules! no_safety_comment {
($t:ty) => {
unsafe impl T for $t {}
};
}
// error
no_safety_comment!(());

macro_rules! with_safety_comment {
($t:ty) => {
// SAFETY:
unsafe impl T for $t {}
};
}
// ok
with_safety_comment!((i32));
}

#[rustfmt::skip]
mod unsafe_impl_valid_comment {
unsafe trait SaFety {}
// SaFety:
unsafe impl SaFety for () {}

unsafe trait MultiLineComment {}
// The following impl is safe
// ...
// Safety: reason
unsafe impl MultiLineComment for () {}

unsafe trait NoAscii {}
// 安全 SAFETY: 以下のコードは安全です
unsafe impl NoAscii for () {}

unsafe trait InlineAndPrecedingComment {}
// SAFETY:
/* comment */ unsafe impl InlineAndPrecedingComment for () {}

unsafe trait BuriedSafety {}
// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
// ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
// reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
// occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
// laborum. Safety:
// Tellus elementum sagittis vitae et leo duis ut diam quam. Sit amet nulla facilisi
// morbi tempus iaculis urna. Amet luctus venenatis lectus magna. At quis risus sed vulputate odio
// ut. Luctus venenatis lectus magna fringilla urna. Tortor id aliquet lectus proin nibh nisl
// condimentum id venenatis. Vulputate dignissim suspendisse in est ante in nibh mauris cursus.
unsafe impl BuriedSafety for () {}

unsafe trait MultiLineBlockComment {}
/* This is a description
* Safety: */
unsafe impl MultiLineBlockComment for () {}
}

#[rustfmt::skip]
mod unsafe_impl_invalid_comment {
unsafe trait NoComment {}

unsafe impl NoComment for () {}

unsafe trait InlineComment {}

/* SAFETY: */ unsafe impl InlineComment for () {}

unsafe trait TrailingComment {}

unsafe impl TrailingComment for () {} // SAFETY:

unsafe trait Interference {}
// SAFETY:
const BIG_NUMBER: i32 = 1000000;
unsafe impl Interference for () {}
}

fn main() {}
Loading