-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add the undocumented_unsafe_blocks
lint
#7557
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
Changes from all commits
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,128 @@ | ||
use clippy_utils::diagnostics::span_lint; | ||
use if_chain::if_chain; | ||
use rustc_ast::ast::{BlockCheckMode, ExprKind, UnsafeSource}; | ||
use rustc_data_structures::fx::FxHashMap; | ||
use rustc_lint::{EarlyContext, EarlyLintPass}; | ||
use rustc_session::{declare_tool_lint, impl_lint_pass}; | ||
use rustc_span::{source_map::SourceMap, BytePos, FileName, Span}; | ||
use std::convert::TryInto; | ||
|
||
declare_clippy_lint! { | ||
/// ### What it does | ||
/// Checks for `unsafe` blocks without a `// SAFETY` comment | ||
/// explaining why the unsafe operations performed inside | ||
/// the block are safe. | ||
/// | ||
/// ### Why is this bad? | ||
/// Undocumented unsafe blocks are hard to check and maintain. | ||
/// On the other hand, writing safety comments helps uncovering | ||
/// unsoundness holes and bugs. | ||
/// | ||
/// ### Example | ||
/// ```rust | ||
/// # use std::ptr::NonNull; | ||
/// # let a = &mut 42; | ||
/// | ||
/// let ptr = unsafe { NonNull::new_unchecked(a) }; | ||
/// ``` | ||
/// You should explain why calling `NonNull::new_unchecked` is safe: | ||
/// ```rust | ||
/// # use std::ptr::NonNull; | ||
/// # let a = &mut 42; | ||
/// | ||
/// // SAFETY: references are guaranteed to be non-null. | ||
/// let ptr = unsafe { NonNull::new_unchecked(a) }; | ||
/// ``` | ||
pub UNDOCUMENTED_UNSAFE_BLOCKS, | ||
restriction, | ||
"unsafe blocks without safety comments" | ||
} | ||
|
||
struct SafetyComment { | ||
hi: BytePos, | ||
hi_line: usize, | ||
} | ||
|
||
#[derive(Default)] | ||
pub struct UndocumentedUnsafeBlocks { | ||
safety_comments: FxHashMap<FileName, Option<Vec<SafetyComment>>>, | ||
} | ||
|
||
impl UndocumentedUnsafeBlocks { | ||
fn safety_comments(&mut self, sm: &SourceMap, span: Span) -> Option<&mut Vec<SafetyComment>> { | ||
let file = sm.span_to_filename(span); | ||
self.safety_comments | ||
.entry(file.clone()) | ||
.or_insert_with(|| Self::gather_safety_comments(sm, &file)) | ||
.as_mut() | ||
} | ||
|
||
// Inspired by `rustc_ast::utils::comments::gather_comments`. | ||
fn gather_safety_comments(sm: &SourceMap, file: &FileName) -> Option<Vec<SafetyComment>> { | ||
let source_file = sm.get_source_file(file)?; | ||
let src = source_file.src.as_deref()?; | ||
let start_bpos = source_file.start_pos; | ||
let mut comments = Vec::new(); | ||
|
||
let mut inside_comment = false; | ||
let mut pos = rustc_lexer::strip_shebang(src).unwrap_or(0); | ||
for token in rustc_lexer::tokenize(&src[pos..]) { | ||
match token.kind { | ||
rustc_lexer::TokenKind::LineComment { doc_style: None } | ||
| rustc_lexer::TokenKind::BlockComment { | ||
doc_style: None, | ||
terminated: true, | ||
} if src[pos + 2..pos + token.len].trim_start().starts_with("SAFETY") => { | ||
inside_comment = true; | ||
}, | ||
rustc_lexer::TokenKind::LineComment { doc_style: None } | ||
| rustc_lexer::TokenKind::BlockComment { doc_style: None, .. } | ||
| rustc_lexer::TokenKind::Whitespace => {}, | ||
_ => { | ||
if inside_comment { | ||
let hi = start_bpos + BytePos(pos.try_into().unwrap()); | ||
comments.push(SafetyComment { | ||
hi, | ||
hi_line: source_file.lookup_file_pos_with_col_display(hi).0, | ||
}); | ||
inside_comment = false; | ||
} | ||
}, | ||
} | ||
pos += token.len; | ||
} | ||
Some(comments) | ||
} | ||
} | ||
|
||
impl_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]); | ||
|
||
impl EarlyLintPass for UndocumentedUnsafeBlocks { | ||
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &rustc_ast::Expr) { | ||
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. There's also a |
||
let (block, comments) = if_chain! { | ||
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 think you can add a |
||
if let ExprKind::Block(ref block, _) = expr.kind; | ||
if let BlockCheckMode::Unsafe(UnsafeSource::UserProvided) = block.rules; | ||
if let Some(comments) = self.safety_comments(cx.sess.source_map(), block.span); | ||
then { | ||
(block, comments) | ||
} | ||
else { | ||
return; | ||
} | ||
}; | ||
if_chain! { | ||
// Since we're consuming comments as we visit the AST, the comment | ||
// we're searching for is likely to be at the beginning of the vector, | ||
// so this is better than a binary search. | ||
if let Some((i, comment)) = comments.iter().enumerate().take_while(|(_, c)| c.hi <= block.span.lo()).last(); | ||
let block_line = cx.sess.source_map().lookup_char_pos(block.span.lo()).line; | ||
if block_line == comment.hi_line + 1 || block_line == comment.hi_line; | ||
then { | ||
comments.remove(i); | ||
} | ||
else { | ||
span_lint(cx, UNDOCUMENTED_UNSAFE_BLOCKS, expr.span, "this `unsafe` block is missing a SAFETY comment"); | ||
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 think we should produce a help message here, suggesting to add a |
||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
#![warn(clippy::undocumented_unsafe_blocks)] | ||
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'd like to see some tests of how this lint behaves in macros. |
||
#![allow(unused_unsafe)] | ||
|
||
fn good_basic() { | ||
// SAFETY | ||
unsafe {} | ||
} | ||
|
||
fn good_newlines() { | ||
// SAFETY | ||
// | ||
// | ||
// | ||
unsafe {} | ||
} | ||
|
||
#[rustfmt::skip] | ||
fn good_whitespaces() { | ||
// SAFETY | ||
|
||
|
||
|
||
unsafe {} | ||
} | ||
|
||
fn good_block() { | ||
/* SAFETY */ | ||
unsafe {} | ||
} | ||
|
||
#[rustfmt::skip] | ||
fn good_inline_block() { | ||
/* SAFETY */ unsafe {} | ||
} | ||
|
||
fn good_statement() { | ||
let _ = | ||
// SAFETY | ||
unsafe {}; | ||
Comment on lines
+37
to
+39
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. There's a test missing for the example in the documentation: // SAFETY: bla
let _ = unsafe {}; |
||
} | ||
|
||
fn bad_no_comment() { | ||
unsafe {} | ||
} | ||
|
||
fn bad_comment_after_block() { | ||
unsafe {} // SAFETY | ||
} | ||
|
||
fn bad_comment_after_block2() { | ||
unsafe { | ||
// | ||
} // SAFETY | ||
// SAFETY | ||
} | ||
|
||
fn bad_comment_inside_block() { | ||
unsafe { | ||
// SAFETY | ||
} | ||
} | ||
|
||
#[rustfmt::skip] | ||
fn bad_two_blocks_one_comment() { | ||
// SAFETY | ||
unsafe {}; unsafe {} | ||
} | ||
|
||
#[rustfmt::skip] | ||
fn bad_block_comment_inside_block() { | ||
unsafe { /* SAFETY */ } | ||
} | ||
|
||
fn main() {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
error: this `unsafe` block is missing a SAFETY comment | ||
--> $DIR/undocumented_unsafe_blocks.rs:43:5 | ||
| | ||
LL | unsafe {} | ||
| ^^^^^^^^^ | ||
| | ||
= note: `-D clippy::undocumented-unsafe-blocks` implied by `-D warnings` | ||
|
||
error: this `unsafe` block is missing a SAFETY comment | ||
--> $DIR/undocumented_unsafe_blocks.rs:47:5 | ||
| | ||
LL | unsafe {} // SAFETY | ||
| ^^^^^^^^^ | ||
|
||
error: this `unsafe` block is missing a SAFETY comment | ||
--> $DIR/undocumented_unsafe_blocks.rs:51:5 | ||
| | ||
LL | / unsafe { | ||
LL | | // | ||
LL | | } // SAFETY | ||
| |_____^ | ||
|
||
error: this `unsafe` block is missing a SAFETY comment | ||
--> $DIR/undocumented_unsafe_blocks.rs:58:5 | ||
| | ||
LL | / unsafe { | ||
LL | | // SAFETY | ||
LL | | } | ||
| |_____^ | ||
|
||
error: this `unsafe` block is missing a SAFETY comment | ||
--> $DIR/undocumented_unsafe_blocks.rs:66:16 | ||
| | ||
LL | unsafe {}; unsafe {} | ||
| ^^^^^^^^^ | ||
|
||
error: this `unsafe` block is missing a SAFETY comment | ||
--> $DIR/undocumented_unsafe_blocks.rs:71:5 | ||
| | ||
LL | unsafe { /* SAFETY */ } | ||
| ^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
error: aborting due to 6 previous errors | ||
|
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.
Since we don't really care about correct and complete syntax in the file, but only finding comments immediately above the
unsafe
block, you should be able to optimize this. I think you can use theget_enclosing_scope
in combination with theopt_span
methods and then slice the to-tokenizesrc
with the start of the enclosing scope up until the start of the block/the start of the line theunsafe
block is in (forlet
stmts).To do this, you would have to turn this lint into a
LatePassLint
, but that is a tradeoff I'm willing to take in order to not re-lex whole files just to find a comment, of which we already know where it should be.To make sure that just slicing the
src
works as expected, please add a test case with unicode chars in the comment and the code above theunsafe
block.With this change, you also might be able to just directly return if the checked block has a
SAFETY
comment right above it, without having to doBytePos
andspan.hi()/.lo()
gymnastics. Basically you have to track if after lexing the suggested part of the file, the last thing you saw was a comment and if it was aSAFETY
comment.