]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/undocumented_unsafe_blocks.rs
Introduce PredicateKind::Clause
[rust.git] / clippy_lints / src / undocumented_unsafe_blocks.rs
1 use clippy_utils::diagnostics::span_lint_and_help;
2 use clippy_utils::source::walk_span_to_context;
3 use clippy_utils::{get_parent_node, is_lint_allowed};
4 use rustc_data_structures::sync::Lrc;
5 use rustc_hir as hir;
6 use rustc_hir::{Block, BlockCheckMode, ItemKind, Node, 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};
12
13 declare_clippy_lint! {
14     /// ### What it does
15     /// Checks for `unsafe` blocks and impls without a `// SAFETY: ` comment
16     /// explaining why the unsafe operations performed inside
17     /// the block are safe.
18     ///
19     /// Note the comment must appear on the line(s) preceding the unsafe block
20     /// with nothing appearing in between. The following is ok:
21     /// ```ignore
22     /// foo(
23     ///     // SAFETY:
24     ///     // This is a valid safety comment
25     ///     unsafe { *x }
26     /// )
27     /// ```
28     /// But neither of these are:
29     /// ```ignore
30     /// // SAFETY:
31     /// // This is not a valid safety comment
32     /// foo(
33     ///     /* SAFETY: Neither is this */ unsafe { *x },
34     /// );
35     /// ```
36     ///
37     /// ### Why is this bad?
38     /// Undocumented unsafe blocks and impls can make it difficult to
39     /// read and maintain code, as well as uncover unsoundness
40     /// and bugs.
41     ///
42     /// ### Example
43     /// ```rust
44     /// use std::ptr::NonNull;
45     /// let a = &mut 42;
46     ///
47     /// let ptr = unsafe { NonNull::new_unchecked(a) };
48     /// ```
49     /// Use instead:
50     /// ```rust
51     /// use std::ptr::NonNull;
52     /// let a = &mut 42;
53     ///
54     /// // SAFETY: references are guaranteed to be non-null.
55     /// let ptr = unsafe { NonNull::new_unchecked(a) };
56     /// ```
57     #[clippy::version = "1.58.0"]
58     pub UNDOCUMENTED_UNSAFE_BLOCKS,
59     restriction,
60     "creating an unsafe block without explaining why it is safe"
61 }
62
63 declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
64
65 impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
66     fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
67         if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
68             && !in_external_macro(cx.tcx.sess, block.span)
69             && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id)
70             && !is_unsafe_from_proc_macro(cx, block.span)
71             && !block_has_safety_comment(cx, block.span)
72             && !block_parents_have_safety_comment(cx, block.hir_id)
73         {
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')
77             } else {
78                 block.span
79             };
80
81             span_lint_and_help(
82                 cx,
83                 UNDOCUMENTED_UNSAFE_BLOCKS,
84                 span,
85                 "unsafe block missing a safety comment",
86                 None,
87                 "consider adding a safety comment on the preceding line",
88             );
89         }
90     }
91
92     fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) {
93         if let hir::ItemKind::Impl(imple) = item.kind
94             && imple.unsafety == hir::Unsafety::Unsafe
95             && !in_external_macro(cx.tcx.sess, item.span)
96             && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id())
97             && !is_unsafe_from_proc_macro(cx, item.span)
98             && !item_has_safety_comment(cx, item)
99         {
100             let source_map = cx.tcx.sess.source_map();
101             let span = if source_map.is_multiline(item.span) {
102                 source_map.span_until_char(item.span, '\n')
103             } else {
104                 item.span
105             };
106
107             span_lint_and_help(
108                 cx,
109                 UNDOCUMENTED_UNSAFE_BLOCKS,
110                 span,
111                 "unsafe impl missing a safety comment",
112                 None,
113                 "consider adding a safety comment on the preceding line",
114             );
115         }
116     }
117 }
118
119 fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool {
120     let source_map = cx.sess().source_map();
121     let file_pos = source_map.lookup_byte_offset(span.lo());
122     file_pos
123         .sf
124         .src
125         .as_deref()
126         .and_then(|src| src.get(file_pos.pos.to_usize()..))
127         .map_or(true, |src| !src.starts_with("unsafe"))
128 }
129
130 // Checks if any parent {expression, statement, block, local, const, static}
131 // has a safety comment
132 fn block_parents_have_safety_comment(cx: &LateContext<'_>, id: hir::HirId) -> bool {
133     if let Some(node) = get_parent_node(cx.tcx, id) {
134         return match node {
135             Node::Expr(expr) => !is_branchy(expr) && span_in_body_has_safety_comment(cx, expr.span),
136             Node::Stmt(hir::Stmt {
137                 kind:
138                     hir::StmtKind::Local(hir::Local { span, .. })
139                     | hir::StmtKind::Expr(hir::Expr { span, .. })
140                     | hir::StmtKind::Semi(hir::Expr { span, .. }),
141                 ..
142             })
143             | Node::Local(hir::Local { span, .. })
144             | Node::Item(hir::Item {
145                 kind: hir::ItemKind::Const(..) | ItemKind::Static(..),
146                 span,
147                 ..
148             }) => span_in_body_has_safety_comment(cx, *span),
149             _ => false,
150         };
151     }
152     false
153 }
154
155 /// Checks if an expression is "branchy", e.g. loop, match/if/etc.
156 fn is_branchy(expr: &hir::Expr<'_>) -> bool {
157     matches!(
158         expr.kind,
159         hir::ExprKind::If(..) | hir::ExprKind::Loop(..) | hir::ExprKind::Match(..)
160     )
161 }
162
163 /// Checks if the lines immediately preceding the block contain a safety comment.
164 fn block_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
165     // This intentionally ignores text before the start of a function so something like:
166     // ```
167     //     // SAFETY: reason
168     //     fn foo() { unsafe { .. } }
169     // ```
170     // won't work. This is to avoid dealing with where such a comment should be place relative to
171     // attributes and doc comments.
172
173     span_from_macro_expansion_has_safety_comment(cx, span) || span_in_body_has_safety_comment(cx, span)
174 }
175
176 /// Checks if the lines immediately preceding the item contain a safety comment.
177 #[allow(clippy::collapsible_match)]
178 fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> bool {
179     if span_from_macro_expansion_has_safety_comment(cx, item.span) {
180         return true;
181     }
182
183     if item.span.ctxt() == SyntaxContext::root() {
184         if let Some(parent_node) = get_parent_node(cx.tcx, item.hir_id()) {
185             let comment_start = match parent_node {
186                 Node::Crate(parent_mod) => {
187                     comment_start_before_impl_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item)
188                 },
189                 Node::Item(parent_item) => {
190                     if let ItemKind::Mod(parent_mod) = &parent_item.kind {
191                         comment_start_before_impl_in_mod(cx, parent_mod, parent_item.span, item)
192                     } else {
193                         // Doesn't support impls in this position. Pretend a comment was found.
194                         return true;
195                     }
196                 },
197                 Node::Stmt(stmt) => {
198                     if let Some(stmt_parent) = get_parent_node(cx.tcx, stmt.hir_id) {
199                         match stmt_parent {
200                             Node::Block(block) => walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo),
201                             _ => {
202                                 // Doesn't support impls in this position. Pretend a comment was found.
203                                 return true;
204                             },
205                         }
206                     } else {
207                         // Problem getting the parent node. Pretend a comment was found.
208                         return true;
209                     }
210                 },
211                 _ => {
212                     // Doesn't support impls in this position. Pretend a comment was found.
213                     return true;
214                 },
215             };
216
217             let source_map = cx.sess().source_map();
218             if let Some(comment_start) = comment_start
219                 && let Ok(unsafe_line) = source_map.lookup_line(item.span.lo())
220                 && let Ok(comment_start_line) = source_map.lookup_line(comment_start)
221                 && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf)
222                 && let Some(src) = unsafe_line.sf.src.as_deref()
223             {
224                 unsafe_line.sf.lines(|lines| {
225                     comment_start_line.line < unsafe_line.line && text_has_safety_comment(
226                         src,
227                         &lines[comment_start_line.line + 1..=unsafe_line.line],
228                         unsafe_line.sf.start_pos.to_usize(),
229                     )
230                 })
231             } else {
232                 // Problem getting source text. Pretend a comment was found.
233                 true
234             }
235         } else {
236             // No parent node. Pretend a comment was found.
237             true
238         }
239     } else {
240         false
241     }
242 }
243
244 fn comment_start_before_impl_in_mod(
245     cx: &LateContext<'_>,
246     parent_mod: &hir::Mod<'_>,
247     parent_mod_span: Span,
248     imple: &hir::Item<'_>,
249 ) -> Option<BytePos> {
250     parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| {
251         if *item_id == imple.item_id() {
252             if idx == 0 {
253                 // mod A { /* comment */ unsafe impl T {} ... }
254                 // ^------------------------------------------^ returns the start of this span
255                 // ^---------------------^ finally checks comments in this range
256                 if let Some(sp) = walk_span_to_context(parent_mod_span, SyntaxContext::root()) {
257                     return Some(sp.lo());
258                 }
259             } else {
260                 // some_item /* comment */ unsafe impl T {}
261                 // ^-------^ returns the end of this span
262                 //         ^---------------^ finally checks comments in this range
263                 let prev_item = cx.tcx.hir().item(parent_mod.item_ids[idx - 1]);
264                 if let Some(sp) = walk_span_to_context(prev_item.span, SyntaxContext::root()) {
265                     return Some(sp.hi());
266                 }
267             }
268         }
269         None
270     })
271 }
272
273 fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
274     let source_map = cx.sess().source_map();
275     let ctxt = span.ctxt();
276     if ctxt == SyntaxContext::root() {
277         false
278     } else {
279         // From a macro expansion. Get the text from the start of the macro declaration to start of the
280         // unsafe block.
281         //     macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
282         //     ^--------------------------------------------^
283         if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
284             && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
285             && Lrc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
286             && let Some(src) = unsafe_line.sf.src.as_deref()
287         {
288             unsafe_line.sf.lines(|lines| {
289                 macro_line.line < unsafe_line.line && text_has_safety_comment(
290                     src,
291                     &lines[macro_line.line + 1..=unsafe_line.line],
292                     unsafe_line.sf.start_pos.to_usize(),
293                 )
294             })
295         } else {
296             // Problem getting source text. Pretend a comment was found.
297             true
298         }
299     }
300 }
301
302 fn get_body_search_span(cx: &LateContext<'_>) -> Option<Span> {
303     let body = cx.enclosing_body?;
304     let map = cx.tcx.hir();
305     let mut span = map.body(body).value.span;
306     for (_, node) in map.parent_iter(body.hir_id) {
307         match node {
308             Node::Expr(e) => span = e.span,
309             Node::Block(_) | Node::Arm(_) | Node::Stmt(_) | Node::Local(_) => (),
310             _ => break,
311         }
312     }
313     Some(span)
314 }
315
316 fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
317     let source_map = cx.sess().source_map();
318     let ctxt = span.ctxt();
319     if ctxt == SyntaxContext::root()
320         && let Some(search_span) = get_body_search_span(cx)
321     {
322         if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
323             && let Some(body_span) = walk_span_to_context(search_span, SyntaxContext::root())
324             && let Ok(body_line) = source_map.lookup_line(body_span.lo())
325             && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
326             && let Some(src) = unsafe_line.sf.src.as_deref()
327         {
328             // Get the text from the start of function body to the unsafe block.
329             //     fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
330             //              ^-------------^
331             unsafe_line.sf.lines(|lines| {
332                 body_line.line < unsafe_line.line && text_has_safety_comment(
333                     src,
334                     &lines[body_line.line + 1..=unsafe_line.line],
335                     unsafe_line.sf.start_pos.to_usize(),
336                 )
337             })
338         } else {
339             // Problem getting source text. Pretend a comment was found.
340             true
341         }
342     } else {
343         false
344     }
345 }
346
347 /// Checks if the given text has a safety comment for the immediately proceeding line.
348 fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> bool {
349     let mut lines = line_starts
350         .array_windows::<2>()
351         .rev()
352         .map_while(|[start, end]| {
353             let start = start.to_usize() - offset;
354             let end = end.to_usize() - offset;
355             src.get(start..end).map(|text| (start, text.trim_start()))
356         })
357         .filter(|(_, text)| !text.is_empty());
358
359     let Some((line_start, line)) = lines.next() else {
360         return false;
361     };
362     // Check for a sequence of line comments.
363     if line.starts_with("//") {
364         let mut line = line;
365         loop {
366             if line.to_ascii_uppercase().contains("SAFETY:") {
367                 return true;
368             }
369             match lines.next() {
370                 Some((_, x)) if x.starts_with("//") => line = x,
371                 _ => return false,
372             }
373         }
374     }
375     // No line comments; look for the start of a block comment.
376     // This will only find them if they are at the start of a line.
377     let (mut line_start, mut line) = (line_start, line);
378     loop {
379         if line.starts_with("/*") {
380             let src = src[line_start..line_starts.last().unwrap().to_usize() - offset].trim_start();
381             let mut tokens = tokenize(src);
382             return src[..tokens.next().unwrap().len as usize]
383                 .to_ascii_uppercase()
384                 .contains("SAFETY:")
385                 && tokens.all(|t| t.kind == TokenKind::Whitespace);
386         }
387         match lines.next() {
388             Some(x) => (line_start, line) = x,
389             None => return false,
390         }
391     }
392 }