]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/undocumented_unsafe_blocks.rs
Resolved conflicts
[rust.git] / clippy_lints / src / undocumented_unsafe_blocks.rs
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 as hir;
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};
12 use std::rc::Rc;
13
14 declare_clippy_lint! {
15     /// ### What it does
16     /// Checks for `unsafe` blocks without a `// SAFETY: ` comment
17     /// explaining why the unsafe operations performed inside
18     /// the block are safe.
19     ///
20     /// Note the comment must appear on the line(s) preceding the unsafe block
21     /// with nothing appearing in between. The following is ok:
22     /// ```ignore
23     /// foo(
24     ///     // SAFETY:
25     ///     // This is a valid safety comment
26     ///     unsafe { *x }
27     /// )
28     /// ```
29     /// But neither of these are:
30     /// ```ignore
31     /// // SAFETY:
32     /// // This is not a valid safety comment
33     /// foo(
34     ///     /* SAFETY: Neither is this */ unsafe { *x },
35     /// );
36     /// ```
37     ///
38     /// ### Why is this bad?
39     /// Undocumented unsafe blocks can make it difficult to
40     /// read and maintain code, as well as uncover unsoundness
41     /// and bugs.
42     ///
43     /// ### Example
44     /// ```rust
45     /// use std::ptr::NonNull;
46     /// let a = &mut 42;
47     ///
48     /// let ptr = unsafe { NonNull::new_unchecked(a) };
49     /// ```
50     /// Use instead:
51     /// ```rust
52     /// use std::ptr::NonNull;
53     /// let a = &mut 42;
54     ///
55     /// // SAFETY: references are guaranteed to be non-null.
56     /// let ptr = unsafe { NonNull::new_unchecked(a) };
57     /// ```
58     #[clippy::version = "1.58.0"]
59     pub UNDOCUMENTED_UNSAFE_BLOCKS,
60     restriction,
61     "creating an unsafe block without explaining why it is safe"
62 }
63
64 declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
65
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)
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_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)
95
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));
100         }
101         // Sort items by start position
102         item_and_spans.sort_by_key(|e| e.1.lo());
103
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)
109             {
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                         // ^--------------------^
115                         todo!();
116                         //mod_span.until(module.spans)
117                     } else {
118                         // unsafe impl S {} /* comment */ unsafe impl T {}
119                         //                 ^-------------^
120                         item_and_spans[idx - 1].1.between(*item_span)
121                     };
122
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()
126                     {
127                         start.line < end.line && text_has_safety_comment(
128                             src,
129                             &start.sf.lines[start.line + 1 ..= end.line],
130                             start.sf.start_pos.to_usize()
131                         )
132                     } else {
133                         // Problem getting source text. Pretend a comment was found.
134                         true
135                     }
136                 };
137
138                 if !impl_has_safety_comment {
139                     span_lint_and_help(
140                         cx,
141                         UNDOCUMENTED_UNSAFE_BLOCKS,
142                         *item_span,
143                         "unsafe impl missing a safety comment",
144                         None,
145                         "consider adding a safety comment on the preceding line",
146                     );
147                 }
148             }
149         }
150     }
151 }
152
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());
156     file_pos
157         .sf
158         .src
159         .as_deref()
160         .and_then(|src| src.get(file_pos.pos.to_usize()..))
161         .map_or(true, |src| !src.starts_with("unsafe"))
162 }
163
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:
167     // ```
168     //     // SAFETY: reason
169     //     fn foo() { unsafe { .. } }
170     // ```
171     // won't work. This is to avoid dealing with where such a comment should be place relative to
172     // attributes and doc comments.
173
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, &macro_line.sf)
183             && let Some(src) = unsafe_line.sf.src.as_deref()
184         {
185             macro_line.line < unsafe_line.line && text_has_safety_comment(
186                 src,
187                 &unsafe_line.sf.lines[macro_line.line + 1..=unsafe_line.line],
188                 unsafe_line.sf.start_pos.to_usize(),
189             )
190         } else {
191             // Problem getting source text. Pretend a comment was found.
192             true
193         }
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()
200     {
201         // Get the text from the start of function body to the unsafe block.
202         //     fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
203         //              ^-------------^
204         body_line.line < unsafe_line.line && text_has_safety_comment(
205             src,
206             &unsafe_line.sf.lines[body_line.line + 1..=unsafe_line.line],
207             unsafe_line.sf.start_pos.to_usize(),
208         )
209     } else {
210         // Problem getting source text. Pretend a comment was found.
211         true
212     }
213 }
214
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>()
219         .rev()
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()))
224         })
225         .filter(|(_, text)| !text.is_empty());
226
227     let Some((line_start, line)) = lines.next() else {
228         return false;
229     };
230     // Check for a sequence of line comments.
231     if line.starts_with("//") {
232         let mut line = line;
233         loop {
234             if line.to_ascii_uppercase().contains("SAFETY:") {
235                 return true;
236             }
237             match lines.next() {
238                 Some((_, x)) if x.starts_with("//") => line = x,
239                 _ => return false,
240             }
241         }
242     }
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);
246     loop {
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()
252                 .contains("SAFETY:")
253                 && tokens.all(|t| t.kind == TokenKind::Whitespace);
254         }
255         match lines.next() {
256             Some(x) => (line_start, line) = x,
257             None => return false,
258         }
259     }
260 }