1 use clippy_utils::diagnostics::span_lint_and_help;
2 use clippy_utils::is_lint_allowed;
3 use clippy_utils::source::walk_span_to_context;
4 use rustc_data_structures::sync::Lrc;
5 use rustc_hir::{Block, BlockCheckMode, UnsafeSource};
6 use rustc_lexer::{tokenize, TokenKind};
7 use rustc_lint::{LateContext, LateLintPass, LintContext};
8 use rustc_middle::lint::in_external_macro;
9 use rustc_session::{declare_lint_pass, declare_tool_lint};
10 use rustc_span::{BytePos, Pos, SyntaxContext};
12 declare_clippy_lint! {
14 /// Checks for `unsafe` blocks without a `// SAFETY: ` comment
15 /// explaining why the unsafe operations performed inside
16 /// the block are safe.
18 /// Note the comment must appear on the line(s) preceding the unsafe block
19 /// with nothing appearing in between. The following is ok:
23 /// // This is a valid safety comment
27 /// But neither of these are:
30 /// // This is not a valid safety comment
32 /// /* SAFETY: Neither is this */ unsafe { *x },
36 /// ### Why is this bad?
37 /// Undocumented unsafe blocks can make it difficult to
38 /// read and maintain code, as well as uncover unsoundness
43 /// use std::ptr::NonNull;
46 /// let ptr = unsafe { NonNull::new_unchecked(a) };
50 /// use std::ptr::NonNull;
53 /// // SAFETY: references are guaranteed to be non-null.
54 /// let ptr = unsafe { NonNull::new_unchecked(a) };
56 #[clippy::version = "1.58.0"]
57 pub UNDOCUMENTED_UNSAFE_BLOCKS,
59 "creating an unsafe block without explaining why it is safe"
62 declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
64 impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
65 fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
66 if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
67 && !in_external_macro(cx.tcx.sess, block.span)
68 && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id)
69 && !is_unsafe_from_proc_macro(cx, block)
70 && !block_has_safety_comment(cx, block)
72 let source_map = cx.tcx.sess.source_map();
73 let span = if source_map.is_multiline(block.span) {
74 source_map.span_until_char(block.span, '\n')
81 UNDOCUMENTED_UNSAFE_BLOCKS,
83 "unsafe block missing a safety comment",
85 "consider adding a safety comment on the preceding line",
91 fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
92 let source_map = cx.sess().source_map();
93 let file_pos = source_map.lookup_byte_offset(block.span.lo());
98 .and_then(|src| src.get(file_pos.pos.to_usize()..))
99 .map_or(true, |src| !src.starts_with("unsafe"))
102 /// Checks if the lines immediately preceding the block contain a safety comment.
103 fn block_has_safety_comment(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
104 // This intentionally ignores text before the start of a function so something like:
107 // fn foo() { unsafe { .. } }
109 // won't work. This is to avoid dealing with where such a comment should be place relative to
110 // attributes and doc comments.
112 let source_map = cx.sess().source_map();
113 let ctxt = block.span.ctxt();
114 if ctxt != SyntaxContext::root() {
115 // From a macro expansion. Get the text from the start of the macro declaration to start of the unsafe block.
116 // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
117 // ^--------------------------------------------^
118 if let Ok(unsafe_line) = source_map.lookup_line(block.span.lo())
119 && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
120 && Lrc::ptr_eq(&unsafe_line.sf, ¯o_line.sf)
121 && let Some(src) = unsafe_line.sf.src.as_deref()
123 macro_line.line < unsafe_line.line && text_has_safety_comment(
125 &unsafe_line.sf.lines[macro_line.line + 1..=unsafe_line.line],
126 unsafe_line.sf.start_pos.to_usize(),
129 // Problem getting source text. Pretend a comment was found.
132 } else if let Ok(unsafe_line) = source_map.lookup_line(block.span.lo())
133 && let Some(body) = cx.enclosing_body
134 && let Some(body_span) = walk_span_to_context(cx.tcx.hir().body(body).value.span, SyntaxContext::root())
135 && let Ok(body_line) = source_map.lookup_line(body_span.lo())
136 && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
137 && let Some(src) = unsafe_line.sf.src.as_deref()
139 // Get the text from the start of function body to the unsafe block.
140 // fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
142 body_line.line < unsafe_line.line && text_has_safety_comment(
144 &unsafe_line.sf.lines[body_line.line + 1..=unsafe_line.line],
145 unsafe_line.sf.start_pos.to_usize(),
148 // Problem getting source text. Pretend a comment was found.
153 /// Checks if the given text has a safety comment for the immediately proceeding line.
154 fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> bool {
155 let mut lines = line_starts
156 .array_windows::<2>()
158 .map_while(|[start, end]| {
159 let start = start.to_usize() - offset;
160 let end = end.to_usize() - offset;
161 src.get(start..end).map(|text| (start, text.trim_start()))
163 .filter(|(_, text)| !text.is_empty());
165 let Some((line_start, line)) = lines.next() else {
168 // Check for a sequence of line comments.
169 if line.starts_with("//") {
172 if line.to_ascii_uppercase().contains("SAFETY:") {
176 Some((_, x)) if x.starts_with("//") => line = x,
181 // No line comments; look for the start of a block comment.
182 // This will only find them if they are at the start of a line.
183 let (mut line_start, mut line) = (line_start, line);
185 if line.starts_with("/*") {
186 let src = src[line_start..line_starts.last().unwrap().to_usize() - offset].trim_start();
187 let mut tokens = tokenize(src);
188 return src[..tokens.next().unwrap().len]
189 .to_ascii_uppercase()
191 && tokens.all(|t| t.kind == TokenKind::Whitespace);
194 Some(x) => (line_start, line) = x,
195 None => return false,