]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs
Merge commit '0eff589afc83e21a03a168497bbab6b4dfbb4ef6' into clippyup
[rust.git] / src / tools / clippy / clippy_lints / src / undocumented_unsafe_blocks.rs
1 use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
2 use clippy_utils::is_lint_allowed;
3 use clippy_utils::source::{indent_of, reindent_multiline, snippet};
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};
14 use std::borrow::Cow;
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 safe.
21     ///
22     /// ### Why is this bad?
23     /// Undocumented unsafe blocks can make it difficult to
24     /// read and maintain code, as well as uncover unsoundness
25     /// and bugs.
26     ///
27     /// ### Example
28     /// ```rust
29     /// use std::ptr::NonNull;
30     /// let a = &mut 42;
31     ///
32     /// let ptr = unsafe { NonNull::new_unchecked(a) };
33     /// ```
34     /// Use instead:
35     /// ```rust
36     /// use std::ptr::NonNull;
37     /// let a = &mut 42;
38     ///
39     /// // SAFETY: references are guaranteed to be non-null.
40     /// let ptr = unsafe { NonNull::new_unchecked(a) };
41     /// ```
42     #[clippy::version = "1.58.0"]
43     pub UNDOCUMENTED_UNSAFE_BLOCKS,
44     restriction,
45     "creating an unsafe block without explaining why it is safe"
46 }
47
48 impl_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
49
50 #[derive(Default)]
51 pub struct UndocumentedUnsafeBlocks {
52     pub local_level: u32,
53     pub local_span: Option<Span>,
54     // The local was already checked for an overall safety comment
55     // There is no need to continue checking the blocks in the local
56     pub local_checked: bool,
57     // Since we can only check the blocks from expanded macros
58     // We have to omit the suggestion due to the actual definition
59     // Not being available to us
60     pub macro_expansion: bool,
61 }
62
63 impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
64     fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
65         if_chain! {
66             if !self.local_checked;
67             if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id);
68             if !in_external_macro(cx.tcx.sess, block.span);
69             if let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules;
70             if let Some(enclosing_scope_hir_id) = cx.tcx.hir().get_enclosing_scope(block.hir_id);
71             if self.block_has_safety_comment(cx.tcx, enclosing_scope_hir_id, block.span) == Some(false);
72             then {
73                 let mut span = block.span;
74
75                 if let Some(local_span) = self.local_span {
76                     span = local_span;
77
78                     let result = self.block_has_safety_comment(cx.tcx, enclosing_scope_hir_id, span);
79
80                     if result.unwrap_or(true) {
81                         self.local_checked = true;
82                         return;
83                     }
84                 }
85
86                 self.lint(cx, span);
87             }
88         }
89     }
90
91     fn check_local(&mut self, cx: &LateContext<'_>, local: &'_ Local<'_>) {
92         if_chain! {
93             if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, local.hir_id);
94             if !in_external_macro(cx.tcx.sess, local.span);
95             if let Some(init) = local.init;
96             then {
97                 self.visit_expr(init);
98
99                 if self.local_level > 0 {
100                     self.local_span = Some(local.span);
101                 }
102             }
103         }
104     }
105
106     fn check_block_post(&mut self, _: &LateContext<'_>, _: &'_ Block<'_>) {
107         self.local_level = self.local_level.saturating_sub(1);
108
109         if self.local_level == 0 {
110             self.local_checked = false;
111             self.local_span = None;
112         }
113     }
114 }
115
116 impl<'hir> Visitor<'hir> for UndocumentedUnsafeBlocks {
117     type Map = Map<'hir>;
118
119     fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
120         NestedVisitorMap::None
121     }
122
123     fn visit_expr(&mut self, ex: &'v Expr<'v>) {
124         match ex.kind {
125             ExprKind::Block(_, _) => self.local_level = self.local_level.saturating_add(1),
126             _ => walk_expr(self, ex),
127         }
128     }
129 }
130
131 impl UndocumentedUnsafeBlocks {
132     fn block_has_safety_comment(&mut self, tcx: TyCtxt<'_>, enclosing_hir_id: HirId, block_span: Span) -> Option<bool> {
133         let map = tcx.hir();
134         let source_map = tcx.sess.source_map();
135
136         let enclosing_scope_span = map.opt_span(enclosing_hir_id)?;
137
138         let between_span = if block_span.from_expansion() {
139             self.macro_expansion = true;
140             enclosing_scope_span.with_hi(block_span.hi()).source_callsite()
141         } else {
142             self.macro_expansion = false;
143             enclosing_scope_span.to(block_span).source_callsite()
144         };
145
146         let file_name = source_map.span_to_filename(between_span);
147         let source_file = source_map.get_source_file(&file_name)?;
148
149         let lex_start = (between_span.lo().0 - source_file.start_pos.0 + 1) as usize;
150         let lex_end = (between_span.hi().0 - source_file.start_pos.0) as usize;
151         let src_str = source_file.src.as_ref()?[lex_start..lex_end].to_string();
152
153         let source_start_pos = source_file.start_pos.0 as usize + lex_start;
154
155         let mut pos = 0;
156         let mut comment = false;
157
158         for token in rustc_lexer::tokenize(&src_str) {
159             match token.kind {
160                 TokenKind::LineComment { doc_style: None }
161                 | TokenKind::BlockComment {
162                     doc_style: None,
163                     terminated: true,
164                 } => {
165                     let comment_str = src_str[pos + 2..pos + token.len].to_ascii_uppercase();
166
167                     if comment_str.contains("SAFETY:") {
168                         comment = true;
169                     }
170                 },
171                 // We need to add all whitespace to `pos` before checking the comment's line number
172                 TokenKind::Whitespace => {},
173                 _ => {
174                     if comment {
175                         // Get the line number of the "comment" (really wherever the trailing whitespace ended)
176                         let comment_line_num = source_file
177                             .lookup_file_pos(BytePos((source_start_pos + pos).try_into().unwrap()))
178                             .0;
179                         // Find the block/local's line number
180                         let block_line_num = tcx.sess.source_map().lookup_char_pos(block_span.lo()).line;
181
182                         // Check the comment is immediately followed by the block/local
183                         if block_line_num == comment_line_num + 1 || block_line_num == comment_line_num {
184                             return Some(true);
185                         }
186
187                         comment = false;
188                     }
189                 },
190             }
191
192             pos += token.len;
193         }
194
195         Some(false)
196     }
197
198     fn lint(&self, cx: &LateContext<'_>, mut span: Span) {
199         let source_map = cx.tcx.sess.source_map();
200
201         if source_map.is_multiline(span) {
202             span = source_map.span_until_char(span, '\n');
203         }
204
205         if self.macro_expansion {
206             span_lint_and_help(
207                 cx,
208                 UNDOCUMENTED_UNSAFE_BLOCKS,
209                 span,
210                 "unsafe block in macro expansion missing a safety comment",
211                 None,
212                 "consider adding a safety comment in the macro definition",
213             );
214         } else {
215             let block_indent = indent_of(cx, span);
216             let suggestion = format!("// SAFETY: ...\n{}", snippet(cx, span, ".."));
217
218             span_lint_and_sugg(
219                 cx,
220                 UNDOCUMENTED_UNSAFE_BLOCKS,
221                 span,
222                 "unsafe block missing a safety comment",
223                 "consider adding a safety comment",
224                 reindent_multiline(Cow::Borrowed(&suggestion), true, block_indent).to_string(),
225                 Applicability::HasPlaceholders,
226             );
227         }
228     }
229 }