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;
6 use rustc_hir::{Block, BlockCheckMode, UnsafeSource};
7 use rustc_lexer::{tokenize, TokenKind};
8 use rustc_lint::{LateContext, LateLintPass, LintContext};
9 use rustc_middle::lint::in_external_macro;
10 use rustc_session::{declare_lint_pass, declare_tool_lint};
11 use rustc_span::{BytePos, Pos, Span, SyntaxContext};
14 declare_clippy_lint! {
16 /// Checks for `unsafe` blocks without a `// SAFETY: ` comment
17 /// explaining why the unsafe operations performed inside
18 /// the block are safe.
20 /// Note the comment must appear on the line(s) preceding the unsafe block
21 /// with nothing appearing in between. The following is ok:
25 /// // This is a valid safety comment
29 /// But neither of these are:
32 /// // This is not a valid safety comment
34 /// /* SAFETY: Neither is this */ unsafe { *x },
38 /// ### Why is this bad?
39 /// Undocumented unsafe blocks can make it difficult to
40 /// read and maintain code, as well as uncover unsoundness
45 /// use std::ptr::NonNull;
48 /// let ptr = unsafe { NonNull::new_unchecked(a) };
52 /// use std::ptr::NonNull;
55 /// // SAFETY: references are guaranteed to be non-null.
56 /// let ptr = unsafe { NonNull::new_unchecked(a) };
58 #[clippy::version = "1.58.0"]
59 pub UNDOCUMENTED_UNSAFE_BLOCKS,
61 "creating an unsafe block without explaining why it is safe"
64 declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
66 impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
67 fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
68 if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
69 && !in_external_macro(cx.tcx.sess, block.span)
70 && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id)
71 && !is_unsafe_from_proc_macro(cx, block)
72 && !block_has_safety_comment(cx, block)
74 let source_map = cx.tcx.sess.source_map();
75 let span = if source_map.is_multiline(block.span) {
76 source_map.span_until_char(block.span, '\n')
83 UNDOCUMENTED_UNSAFE_BLOCKS,
85 "unsafe block missing a safety comment",
87 "consider adding a safety comment on the preceding line",
92 fn check_mod(&mut self, cx: &LateContext<'_>, module: &'_ hir::Mod<'_>, mod_span: Span, hir_id: hir::HirId) {
93 let source_map = cx.sess().source_map();
94 let mut item_and_spans: Vec<(&hir::Item<'_>, Span)> = Vec::new(); // (start, end, item)
96 // Collect all items and their spans
97 for item_id in module.item_ids {
98 let item = cx.tcx.hir().item(*item_id);
99 item_and_spans.push((item, item.span));
101 // Sort items by start position
102 item_and_spans.sort_by_key(|e| e.1.lo());
104 for (idx, (item, item_span)) in item_and_spans.iter().enumerate() {
105 if let hir::ItemKind::Impl(imple) = &item.kind
106 && imple.unsafety == hir::Unsafety::Unsafe
107 && !item_span.from_expansion()
108 && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, hir_id)
110 // Checks if the lines immediately preceding the impl contain a safety comment.
111 let impl_has_safety_comment = {
112 let span_before_impl = if idx == 0 {
113 // mod A { /* comment */ unsafe impl T {} }
114 // ^--------------------^
116 //mod_span.until(module.spans)
118 // unsafe impl S {} /* comment */ unsafe impl T {}
120 item_and_spans[idx - 1].1.between(*item_span)
123 if let Ok(start) = source_map.lookup_line(span_before_impl.lo())
124 && let Ok(end) = source_map.lookup_line(span_before_impl.hi())
125 && let Some(src) = start.sf.src.as_deref()
127 start.line < end.line && text_has_safety_comment(
129 &start.sf.lines[start.line + 1 ..= end.line],
130 start.sf.start_pos.to_usize()
133 // Problem getting source text. Pretend a comment was found.
138 if !impl_has_safety_comment {
141 UNDOCUMENTED_UNSAFE_BLOCKS,
143 "unsafe impl missing a safety comment",
145 "consider adding a safety comment on the preceding line",
153 fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
154 let source_map = cx.sess().source_map();
155 let file_pos = source_map.lookup_byte_offset(block.span.lo());
160 .and_then(|src| src.get(file_pos.pos.to_usize()..))
161 .map_or(true, |src| !src.starts_with("unsafe"))
164 /// Checks if the lines immediately preceding the block contain a safety comment.
165 fn block_has_safety_comment(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
166 // This intentionally ignores text before the start of a function so something like:
169 // fn foo() { unsafe { .. } }
171 // won't work. This is to avoid dealing with where such a comment should be place relative to
172 // attributes and doc comments.
174 let source_map = cx.sess().source_map();
175 let ctxt = block.span.ctxt();
176 if ctxt != SyntaxContext::root() {
177 // From a macro expansion. Get the text from the start of the macro declaration to start of the unsafe block.
178 // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
179 // ^--------------------------------------------^
180 if let Ok(unsafe_line) = source_map.lookup_line(block.span.lo())
181 && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
182 && Lrc::ptr_eq(&unsafe_line.sf, ¯o_line.sf)
183 && let Some(src) = unsafe_line.sf.src.as_deref()
185 macro_line.line < unsafe_line.line && text_has_safety_comment(
187 &unsafe_line.sf.lines[macro_line.line + 1..=unsafe_line.line],
188 unsafe_line.sf.start_pos.to_usize(),
191 // Problem getting source text. Pretend a comment was found.
194 } else if let Ok(unsafe_line) = source_map.lookup_line(block.span.lo())
195 && let Some(body) = cx.enclosing_body
196 && let Some(body_span) = walk_span_to_context(cx.tcx.hir().body(body).value.span, SyntaxContext::root())
197 && let Ok(body_line) = source_map.lookup_line(body_span.lo())
198 && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
199 && let Some(src) = unsafe_line.sf.src.as_deref()
201 // Get the text from the start of function body to the unsafe block.
202 // fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
204 body_line.line < unsafe_line.line && text_has_safety_comment(
206 &unsafe_line.sf.lines[body_line.line + 1..=unsafe_line.line],
207 unsafe_line.sf.start_pos.to_usize(),
210 // Problem getting source text. Pretend a comment was found.
215 /// Checks if the given text has a safety comment for the immediately proceeding line.
216 fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> bool {
217 let mut lines = line_starts
218 .array_windows::<2>()
220 .map_while(|[start, end]| {
221 let start = start.to_usize() - offset;
222 let end = end.to_usize() - offset;
223 src.get(start..end).map(|text| (start, text.trim_start()))
225 .filter(|(_, text)| !text.is_empty());
227 let Some((line_start, line)) = lines.next() else {
230 // Check for a sequence of line comments.
231 if line.starts_with("//") {
234 if line.to_ascii_uppercase().contains("SAFETY:") {
238 Some((_, x)) if x.starts_with("//") => line = x,
243 // No line comments; look for the start of a block comment.
244 // This will only find them if they are at the start of a line.
245 let (mut line_start, mut line) = (line_start, line);
247 if line.starts_with("/*") {
248 let src = src[line_start..line_starts.last().unwrap().to_usize() - offset].trim_start();
249 let mut tokens = tokenize(src);
250 return src[..tokens.next().unwrap().len]
251 .to_ascii_uppercase()
253 && tokens.all(|t| t.kind == TokenKind::Whitespace);
256 Some(x) => (line_start, line) = x,
257 None => return false,