1 use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
2 use clippy_utils::source::{indent_of, reindent_multiline, snippet};
3 use clippy_utils::{in_macro, is_lint_allowed};
4 use rustc_errors::Applicability;
5 use rustc_hir::intravisit::{walk_expr, NestedVisitorMap, Visitor};
6 use rustc_hir::{Block, BlockCheckMode, Expr, ExprKind, HirId, Local, UnsafeSource};
7 use rustc_lexer::TokenKind;
8 use rustc_lint::{LateContext, LateLintPass};
9 use rustc_middle::hir::map::Map;
10 use rustc_middle::lint::in_external_macro;
11 use rustc_middle::ty::TyCtxt;
12 use rustc_session::{declare_tool_lint, impl_lint_pass};
13 use rustc_span::{BytePos, Span};
16 declare_clippy_lint! {
18 /// Checks for `unsafe` blocks without a `// Safety: ` comment
19 /// explaining why the unsafe operations performed inside
20 /// the block are safe.
22 /// ### Why is this bad?
23 /// Undocumented unsafe blocks can make it difficult to
24 /// read and maintain code, as well as uncover unsoundness
29 /// use std::ptr::NonNull;
32 /// let ptr = unsafe { NonNull::new_unchecked(a) };
36 /// use std::ptr::NonNull;
39 /// // Safety: references are guaranteed to be non-null.
40 /// let ptr = unsafe { NonNull::new_unchecked(a) };
42 pub UNDOCUMENTED_UNSAFE_BLOCKS,
44 "creating an unsafe block without explaining why it is safe"
47 impl_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
50 pub struct UndocumentedUnsafeBlocks {
52 pub local_span: Option<Span>,
53 // The local was already checked for an overall safety comment
54 // There is no need to continue checking the blocks in the local
55 pub local_checked: bool,
56 // Since we can only check the blocks from expanded macros
57 // We have to omit the suggestion due to the actual definition
58 // Not being available to us
59 pub macro_expansion: bool,
62 impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
63 fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
65 if !self.local_checked;
66 if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id);
67 if !in_external_macro(cx.tcx.sess, block.span);
68 if let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules;
69 if let Some(enclosing_scope_hir_id) = cx.tcx.hir().get_enclosing_scope(block.hir_id);
70 if self.block_has_safety_comment(cx.tcx, enclosing_scope_hir_id, block.span) == Some(false);
72 let mut span = block.span;
74 if let Some(local_span) = self.local_span {
77 let result = self.block_has_safety_comment(cx.tcx, enclosing_scope_hir_id, span);
79 if result.unwrap_or(true) {
80 self.local_checked = true;
90 fn check_local(&mut self, cx: &LateContext<'_>, local: &'_ Local<'_>) {
92 if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, local.hir_id);
93 if !in_external_macro(cx.tcx.sess, local.span);
94 if let Some(init) = local.init;
96 self.visit_expr(init);
98 if self.local_level > 0 {
99 self.local_span = Some(local.span);
105 fn check_block_post(&mut self, _: &LateContext<'_>, _: &'_ Block<'_>) {
106 self.local_level = self.local_level.saturating_sub(1);
108 if self.local_level == 0 {
109 self.local_checked = false;
110 self.local_span = None;
115 impl<'hir> Visitor<'hir> for UndocumentedUnsafeBlocks {
116 type Map = Map<'hir>;
118 fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
119 NestedVisitorMap::None
122 fn visit_expr(&mut self, ex: &'v Expr<'v>) {
124 ExprKind::Block(_, _) => self.local_level = self.local_level.saturating_add(1),
125 _ => walk_expr(self, ex),
130 impl UndocumentedUnsafeBlocks {
131 fn block_has_safety_comment(&mut self, tcx: TyCtxt<'_>, enclosing_hir_id: HirId, block_span: Span) -> Option<bool> {
133 let source_map = tcx.sess.source_map();
135 let enclosing_scope_span = map.opt_span(enclosing_hir_id)?;
137 let between_span = if in_macro(block_span) {
138 self.macro_expansion = true;
139 enclosing_scope_span.with_hi(block_span.hi())
141 self.macro_expansion = false;
142 enclosing_scope_span.to(block_span)
145 let file_name = source_map.span_to_filename(between_span);
146 let source_file = source_map.get_source_file(&file_name)?;
148 let lex_start = (between_span.lo().0 + 1) as usize;
149 let src_str = source_file.src.as_ref()?[lex_start..between_span.hi().0 as usize].to_string();
152 let mut comment = false;
154 for token in rustc_lexer::tokenize(&src_str) {
156 TokenKind::LineComment { doc_style: None }
157 | TokenKind::BlockComment {
161 let comment_str = src_str[pos + 2..pos + token.len].to_ascii_uppercase();
163 if comment_str.contains("SAFETY:") {
167 // We need to add all whitespace to `pos` before checking the comment's line number
168 TokenKind::Whitespace => {},
171 // Get the line number of the "comment" (really wherever the trailing whitespace ended)
172 let comment_line_num = source_file
173 .lookup_file_pos_with_col_display(BytePos((lex_start + pos).try_into().unwrap()))
175 // Find the block/local's line number
176 let block_line_num = tcx.sess.source_map().lookup_char_pos(block_span.lo()).line;
178 // Check the comment is immediately followed by the block/local
179 if block_line_num == comment_line_num + 1 || block_line_num == comment_line_num {
194 fn lint(&self, cx: &LateContext<'_>, mut span: Span) {
195 let source_map = cx.tcx.sess.source_map();
197 if source_map.is_multiline(span) {
198 span = source_map.span_until_char(span, '\n');
201 if self.macro_expansion {
204 UNDOCUMENTED_UNSAFE_BLOCKS,
206 "unsafe block in macro expansion missing a safety comment",
208 "consider adding a safety comment in the macro definition",
211 let block_indent = indent_of(cx, span);
212 let suggestion = format!("// Safety: ...\n{}", snippet(cx, span, ".."));
216 UNDOCUMENTED_UNSAFE_BLOCKS,
218 "unsafe block missing a safety comment",
219 "consider adding a safety comment",
220 reindent_multiline(Cow::Borrowed(&suggestion), true, block_indent).to_string(),
221 Applicability::HasPlaceholders,