Skip to content

Commit 521928c

Browse files
committed
Add undocumented_unsafe_block_safety lint
1 parent abe551e commit 521928c

7 files changed

+403
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3032,6 +3032,7 @@ Released 2018-09-13
30323032
[`try_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#try_err
30333033
[`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
30343034
[`type_repetition_in_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds
3035+
[`undocumented_unsafe_block_safety`]: https://rust-lang.github.io/rust-clippy/master/index.html#undocumented_unsafe_block_safety
30353036
[`undropped_manually_drops`]: https://rust-lang.github.io/rust-clippy/master/index.html#undropped_manually_drops
30363037
[`unicode_not_nfc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unicode_not_nfc
30373038
[`unimplemented`]: https://rust-lang.github.io/rust-clippy/master/index.html#unimplemented

clippy_lints/src/lib.mods.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ mod transmute;
204204
mod transmuting_null;
205205
mod try_err;
206206
mod types;
207+
mod undocumented_unsafe_block_safety;
207208
mod undropped_manually_drops;
208209
mod unicode;
209210
mod unit_return_expecting_ord;

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ store.register_lints(&[
464464
types::REDUNDANT_ALLOCATION,
465465
types::TYPE_COMPLEXITY,
466466
types::VEC_BOX,
467+
undocumented_unsafe_block_safety::UNDOCUMENTED_UNSAFE_BLOCK_SAFETY,
467468
undropped_manually_drops::UNDROPPED_MANUALLY_DROPS,
468469
unicode::INVISIBLE_CHARACTERS,
469470
unicode::NON_ASCII_LITERAL,

clippy_lints/src/lib.register_restriction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ store.register_group(true, "clippy::restriction", Some("clippy_restriction"), ve
5656
LintId::of(strings::STR_TO_STRING),
5757
LintId::of(types::RC_BUFFER),
5858
LintId::of(types::RC_MUTEX),
59+
LintId::of(undocumented_unsafe_block_safety::UNDOCUMENTED_UNSAFE_BLOCK_SAFETY),
5960
LintId::of(unnecessary_self_imports::UNNECESSARY_SELF_IMPORTS),
6061
LintId::of(unwrap_in_result::UNWRAP_IN_RESULT),
6162
LintId::of(verbose_file_reads::VERBOSE_FILE_READS),
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::source::{snippet, indent_of, reindent_multiline};
3+
use rustc_errors::Applicability;
4+
use rustc_hir::{Block, BlockCheckMode, ExprKind, HirId, Local, UnsafeSource};
5+
use rustc_lexer::TokenKind;
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_middle::lint::in_external_macro;
8+
use rustc_middle::ty::TyCtxt;
9+
use rustc_session::{impl_lint_pass, declare_tool_lint};
10+
use rustc_span::Span;
11+
use core::str::Lines;
12+
use core::iter::Rev;
13+
use std::borrow::Cow;
14+
use clippy_utils::is_lint_allowed;
15+
16+
declare_clippy_lint! {
17+
/// ### What it does
18+
/// Checks for `unsafe` blocks without a `// Safety: ` comment
19+
/// explaining why the unsafe operations performed inside
20+
/// the block are (or are not) safe.
21+
///
22+
/// ### Why is this bad?
23+
/// Unsafe blocks without a safety comment can be difficult to
24+
/// understand for those that are unfamiliar with the block's code
25+
/// and/or unsafe in general.
26+
///
27+
/// ### Example
28+
/// ```rust
29+
/// unsafe {};
30+
///
31+
/// let _ = unsafe {};
32+
/// ```
33+
/// Use instead:
34+
/// ```rust
35+
/// // Safety: ...
36+
/// unsafe {}
37+
///
38+
/// // Safety: ...
39+
/// let _ = unsafe {};
40+
/// ```
41+
pub UNDOCUMENTED_UNSAFE_BLOCK_SAFETY,
42+
restriction,
43+
"creating an unsafe block without explaining why it is or is not safe"
44+
}
45+
46+
impl_lint_pass!(UndocumentedUnsafeBlockSafety => [UNDOCUMENTED_UNSAFE_BLOCK_SAFETY]);
47+
48+
#[derive(Default)]
49+
pub struct UndocumentedUnsafeBlockSafety {
50+
pub local: bool
51+
}
52+
53+
#[derive(PartialEq)]
54+
enum CommentType {
55+
Safety,
56+
Unrelated,
57+
None,
58+
}
59+
60+
impl LateLintPass<'_> for UndocumentedUnsafeBlockSafety {
61+
fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
62+
if self.local {
63+
self.local = false;
64+
return
65+
}
66+
67+
if_chain! {
68+
if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCK_SAFETY, block.hir_id);
69+
if !in_external_macro(cx.tcx.sess, block.span);
70+
if let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules;
71+
if let Some(enclosing_scope_hir_id) = cx.tcx.hir().get_enclosing_scope(block.hir_id);
72+
then {
73+
find_candidate(cx, block.span, enclosing_scope_hir_id)
74+
}
75+
}
76+
}
77+
78+
fn check_local(&mut self, cx: &LateContext<'_>, local: &'_ Local<'_>) {
79+
if_chain! {
80+
if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCK_SAFETY, local.hir_id);
81+
if !in_external_macro(cx.tcx.sess, local.span);
82+
if let Some(init) = local.init;
83+
if let ExprKind::Block(block, _) = init.kind;
84+
if let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules;
85+
if let Some(enclosing_scope_hir_id) = cx.tcx.hir().get_enclosing_scope(block.hir_id);
86+
then {
87+
self.local = true;
88+
find_candidate(cx, local.span, enclosing_scope_hir_id)
89+
}
90+
}
91+
}
92+
}
93+
94+
fn find_candidate(cx: &LateContext<'_>, span: Span, enclosing_hir_id: HirId) {
95+
if let Some(true) = block_has_safety_comment(cx.tcx, enclosing_hir_id, span) {
96+
return;
97+
}
98+
99+
let block_indent = indent_of(cx, span);
100+
let suggestion = format!("// Safety: ...\n{}", snippet(cx, span, ".."));
101+
102+
span_lint_and_sugg(
103+
cx,
104+
UNDOCUMENTED_UNSAFE_BLOCK_SAFETY,
105+
span,
106+
"unsafe block missing a safety comment",
107+
"consider adding a safety comment",
108+
reindent_multiline(Cow::Borrowed(&suggestion), true, block_indent).to_string(),
109+
Applicability::HasPlaceholders,
110+
);
111+
}
112+
113+
fn block_has_safety_comment(tcx: TyCtxt<'_>, enclosing_hir_id: HirId, block_span: Span) -> Option<bool> {
114+
let map = tcx.hir();
115+
let source_map = tcx.sess.source_map();
116+
117+
let enclosing_scope_span = map.opt_span(enclosing_hir_id)?;
118+
let between_span = enclosing_scope_span.until(block_span);
119+
120+
let file_name = source_map.span_to_filename(between_span);
121+
let source_file = source_map.get_source_file(&file_name)?;
122+
123+
let lex_start = (between_span.lo().0 + 1) as usize;
124+
let mut src_str = source_file.src.as_ref()?[lex_start..between_span.hi().0 as usize].to_string();
125+
126+
// Remove all whitespace, but retain newlines to verify the block immediately follows the comment
127+
src_str.retain(|c| c == '\n' || !c.is_whitespace());
128+
129+
let mut src_str_split = src_str.lines().rev();
130+
131+
let mut found_safety_comment = false;
132+
133+
while let Some(line) = src_str_split.next() {
134+
match line {
135+
"*/" => return Some(check_multiline_block(&mut src_str_split, None)),
136+
line if (line.starts_with('*') && line.ends_with("*/")) => return Some(check_multiline_block(&mut src_str_split, Some(&line[1..]))),
137+
// Covers both line comments and single line block comments
138+
line => match contains_safety_comment(line, false) {
139+
CommentType::Safety => { found_safety_comment = true; break },
140+
CommentType::Unrelated => continue,
141+
CommentType::None => break,
142+
}
143+
}
144+
}
145+
146+
Some(found_safety_comment)
147+
}
148+
149+
fn check_multiline_block(lines: &mut Rev<Lines<'_>>, same_line_terminator: Option<&str>) -> bool {
150+
let mut found_safety = false;
151+
152+
if let Some(line) = same_line_terminator {
153+
found_safety = contains_safety_comment(line, true) == CommentType::Safety;
154+
}
155+
156+
for next_line in lines {
157+
if found_safety {
158+
break
159+
}
160+
161+
let text_start = if next_line.starts_with('*') {
162+
1
163+
} else if next_line.starts_with("/*") {
164+
2
165+
} else {
166+
return false
167+
};
168+
169+
found_safety = contains_safety_comment(&next_line[text_start..], true) == CommentType::Safety;
170+
}
171+
172+
found_safety
173+
}
174+
175+
fn contains_safety_comment(comment: &str, block: bool) -> CommentType {
176+
if block || is_comment(comment) {
177+
let comment_upper = comment.to_uppercase();
178+
if comment_upper.trim_start().starts_with("SAFETY:") || comment_upper.contains("SAFETY:") {
179+
return CommentType::Safety
180+
}
181+
182+
return CommentType::Unrelated
183+
}
184+
185+
CommentType::None
186+
}
187+
188+
fn is_comment(comment: &str) -> bool {
189+
if let Some(token) = rustc_lexer::tokenize(comment).next() {
190+
match token.kind {
191+
TokenKind::LineComment { doc_style: None }
192+
| TokenKind::BlockComment { doc_style: None, terminated: true } => return true,
193+
_ => {},
194+
}
195+
}
196+
197+
false
198+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#![warn(clippy::undocumented_unsafe_block_safety)]
2+
3+
// Valid comments
4+
5+
fn line_comment() {
6+
// Safety:
7+
unsafe {}
8+
}
9+
10+
fn line_comment_with_extras() {
11+
// This is a description
12+
// Safety:
13+
unsafe {}
14+
}
15+
16+
fn let_statement_line_comment() {
17+
// Safety:
18+
let _ = unsafe {};
19+
}
20+
21+
fn block_comment() {
22+
/* Safety: */
23+
unsafe {}
24+
}
25+
26+
#[rustfmt::skip]
27+
fn inline_block_comment() {
28+
/* Safety: */unsafe {}
29+
}
30+
31+
fn block_comment_with_extras() {
32+
/* This is a description
33+
* Safety:
34+
*/
35+
unsafe {}
36+
}
37+
38+
fn block_comment_terminator_same_line() {
39+
/* This is a description
40+
* Safety: */
41+
unsafe {}
42+
}
43+
44+
fn let_statement_block_comment() {
45+
/* Safety: */
46+
let _ = unsafe {};
47+
}
48+
49+
fn buried_safety() {
50+
// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
51+
// incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
52+
// ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
53+
// reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
54+
// occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
55+
// laborum. Safety:
56+
// Tellus elementum sagittis vitae et leo duis ut diam quam. Sit amet nulla facilisi
57+
// morbi tempus iaculis urna. Amet luctus venenatis lectus magna. At quis risus sed vulputate odio
58+
// ut. Luctus venenatis lectus magna fringilla urna. Tortor id aliquet lectus proin nibh nisl
59+
// condimentum id venenatis. Vulputate dignissim suspendisse in est ante in nibh mauris cursus.
60+
unsafe {}
61+
}
62+
63+
fn safety_with_prepended_text() {
64+
// This is a test. Safety:
65+
unsafe {}
66+
}
67+
68+
// Invalid comments
69+
70+
fn no_comment() {
71+
unsafe {}
72+
}
73+
74+
fn let_statement_no_comment() {
75+
let _ = unsafe {};
76+
}
77+
78+
fn let_statement_commented_block() {
79+
let _ =
80+
// Safety:
81+
unsafe {};
82+
}
83+
84+
fn trailing_comment() {
85+
unsafe {} // Safety:
86+
}
87+
88+
fn internal_comment() {
89+
unsafe {
90+
// Safety:
91+
}
92+
}
93+
94+
fn line_comment_newlines() {
95+
// Safety:
96+
97+
unsafe {}
98+
}
99+
100+
fn block_comment_newlines() {
101+
/* Safety: */
102+
103+
unsafe {}
104+
}
105+
106+
fn main() {}

0 commit comments

Comments
 (0)