]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs
Rollup merge of #97130 - notriddle:notriddle/collect-trait-impls-dup, r=GuillaumeGomez
[rust.git] / src / tools / clippy / 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)
72         {
73             let source_map = cx.tcx.sess.source_map();
74             let span = if source_map.is_multiline(block.span) {
75                 source_map.span_until_char(block.span, '\n')
76             } else {
77                 block.span
78             };
79
80             span_lint_and_help(
81                 cx,
82                 UNDOCUMENTED_UNSAFE_BLOCKS,
83                 span,
84                 "unsafe block missing a safety comment",
85                 None,
86                 "consider adding a safety comment on the preceding line",
87             );
88         }
89     }
90
91     fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) {
92         if let hir::ItemKind::Impl(imple) = item.kind
93             && imple.unsafety == hir::Unsafety::Unsafe
94             && !in_external_macro(cx.tcx.sess, item.span)
95             && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id())
96             && !is_unsafe_from_proc_macro(cx, item.span)
97             && !item_has_safety_comment(cx, item)
98         {
99             let source_map = cx.tcx.sess.source_map();
100             let span = if source_map.is_multiline(item.span) {
101                 source_map.span_until_char(item.span, '\n')
102             } else {
103                 item.span
104             };
105
106             span_lint_and_help(
107                 cx,
108                 UNDOCUMENTED_UNSAFE_BLOCKS,
109                 span,
110                 "unsafe impl missing a safety comment",
111                 None,
112                 "consider adding a safety comment on the preceding line",
113             );
114         }
115     }
116 }
117
118 fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool {
119     let source_map = cx.sess().source_map();
120     let file_pos = source_map.lookup_byte_offset(span.lo());
121     file_pos
122         .sf
123         .src
124         .as_deref()
125         .and_then(|src| src.get(file_pos.pos.to_usize()..))
126         .map_or(true, |src| !src.starts_with("unsafe"))
127 }
128
129 /// Checks if the lines immediately preceding the block contain a safety comment.
130 fn block_has_safety_comment(cx: &LateContext<'_>, block: &hir::Block<'_>) -> bool {
131     // This intentionally ignores text before the start of a function so something like:
132     // ```
133     //     // SAFETY: reason
134     //     fn foo() { unsafe { .. } }
135     // ```
136     // won't work. This is to avoid dealing with where such a comment should be place relative to
137     // attributes and doc comments.
138
139     span_from_macro_expansion_has_safety_comment(cx, block.span) || span_in_body_has_safety_comment(cx, block.span)
140 }
141
142 /// Checks if the lines immediately preceding the item contain a safety comment.
143 #[allow(clippy::collapsible_match)]
144 fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> bool {
145     if span_from_macro_expansion_has_safety_comment(cx, item.span) {
146         return true;
147     }
148
149     if item.span.ctxt() == SyntaxContext::root() {
150         if let Some(parent_node) = get_parent_node(cx.tcx, item.hir_id()) {
151             let comment_start = match parent_node {
152                 Node::Crate(parent_mod) => {
153                     comment_start_before_impl_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item)
154                 },
155                 Node::Item(parent_item) => {
156                     if let ItemKind::Mod(parent_mod) = &parent_item.kind {
157                         comment_start_before_impl_in_mod(cx, parent_mod, parent_item.span, item)
158                     } else {
159                         // Doesn't support impls in this position. Pretend a comment was found.
160                         return true;
161                     }
162                 },
163                 Node::Stmt(stmt) => {
164                     if let Some(stmt_parent) = get_parent_node(cx.tcx, stmt.hir_id) {
165                         match stmt_parent {
166                             Node::Block(block) => walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo),
167                             _ => {
168                                 // Doesn't support impls in this position. Pretend a comment was found.
169                                 return true;
170                             },
171                         }
172                     } else {
173                         // Problem getting the parent node. Pretend a comment was found.
174                         return true;
175                     }
176                 },
177                 _ => {
178                     // Doesn't support impls in this position. Pretend a comment was found.
179                     return true;
180                 },
181             };
182
183             let source_map = cx.sess().source_map();
184             if let Some(comment_start) = comment_start
185                 && let Ok(unsafe_line) = source_map.lookup_line(item.span.lo())
186                 && let Ok(comment_start_line) = source_map.lookup_line(comment_start)
187                 && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf)
188                 && let Some(src) = unsafe_line.sf.src.as_deref()
189             {
190                 comment_start_line.line < unsafe_line.line && text_has_safety_comment(
191                     src,
192                     &unsafe_line.sf.lines[comment_start_line.line + 1..=unsafe_line.line],
193                     unsafe_line.sf.start_pos.to_usize(),
194                 )
195             } else {
196                 // Problem getting source text. Pretend a comment was found.
197                 true
198             }
199         } else {
200             // No parent node. Pretend a comment was found.
201             true
202         }
203     } else {
204         false
205     }
206 }
207
208 fn comment_start_before_impl_in_mod(
209     cx: &LateContext<'_>,
210     parent_mod: &hir::Mod<'_>,
211     parent_mod_span: Span,
212     imple: &hir::Item<'_>,
213 ) -> Option<BytePos> {
214     parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| {
215         if *item_id == imple.item_id() {
216             if idx == 0 {
217                 // mod A { /* comment */ unsafe impl T {} ... }
218                 // ^------------------------------------------^ returns the start of this span
219                 // ^---------------------^ finally checks comments in this range
220                 if let Some(sp) = walk_span_to_context(parent_mod_span, SyntaxContext::root()) {
221                     return Some(sp.lo());
222                 }
223             } else {
224                 // some_item /* comment */ unsafe impl T {}
225                 // ^-------^ returns the end of this span
226                 //         ^---------------^ finally checks comments in this range
227                 let prev_item = cx.tcx.hir().item(parent_mod.item_ids[idx - 1]);
228                 if let Some(sp) = walk_span_to_context(prev_item.span, SyntaxContext::root()) {
229                     return Some(sp.hi());
230                 }
231             }
232         }
233         None
234     })
235 }
236
237 fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
238     let source_map = cx.sess().source_map();
239     let ctxt = span.ctxt();
240     if ctxt == SyntaxContext::root() {
241         false
242     } else {
243         // From a macro expansion. Get the text from the start of the macro declaration to start of the
244         // unsafe block.
245         //     macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
246         //     ^--------------------------------------------^
247         if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
248             && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
249             && Lrc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
250             && let Some(src) = unsafe_line.sf.src.as_deref()
251         {
252             macro_line.line < unsafe_line.line && text_has_safety_comment(
253                 src,
254                 &unsafe_line.sf.lines[macro_line.line + 1..=unsafe_line.line],
255                 unsafe_line.sf.start_pos.to_usize(),
256             )
257         } else {
258             // Problem getting source text. Pretend a comment was found.
259             true
260         }
261     }
262 }
263
264 fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
265     let source_map = cx.sess().source_map();
266     let ctxt = span.ctxt();
267     if ctxt == SyntaxContext::root()
268         && let Some(body) = cx.enclosing_body
269     {
270         if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
271             && let Some(body_span) = walk_span_to_context(cx.tcx.hir().body(body).value.span, SyntaxContext::root())
272             && let Ok(body_line) = source_map.lookup_line(body_span.lo())
273             && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
274             && let Some(src) = unsafe_line.sf.src.as_deref()
275         {
276             // Get the text from the start of function body to the unsafe block.
277             //     fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
278             //              ^-------------^
279             body_line.line < unsafe_line.line && text_has_safety_comment(
280                 src,
281                 &unsafe_line.sf.lines[body_line.line + 1..=unsafe_line.line],
282                 unsafe_line.sf.start_pos.to_usize(),
283             )
284         } else {
285             // Problem getting source text. Pretend a comment was found.
286             true
287         }
288     } else {
289         false
290     }
291 }
292
293 /// Checks if the given text has a safety comment for the immediately proceeding line.
294 fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> bool {
295     let mut lines = line_starts
296         .array_windows::<2>()
297         .rev()
298         .map_while(|[start, end]| {
299             let start = start.to_usize() - offset;
300             let end = end.to_usize() - offset;
301             src.get(start..end).map(|text| (start, text.trim_start()))
302         })
303         .filter(|(_, text)| !text.is_empty());
304
305     let Some((line_start, line)) = lines.next() else {
306         return false;
307     };
308     // Check for a sequence of line comments.
309     if line.starts_with("//") {
310         let mut line = line;
311         loop {
312             if line.to_ascii_uppercase().contains("SAFETY:") {
313                 return true;
314             }
315             match lines.next() {
316                 Some((_, x)) if x.starts_with("//") => line = x,
317                 _ => return false,
318             }
319         }
320     }
321     // No line comments; look for the start of a block comment.
322     // This will only find them if they are at the start of a line.
323     let (mut line_start, mut line) = (line_start, line);
324     loop {
325         if line.starts_with("/*") {
326             let src = src[line_start..line_starts.last().unwrap().to_usize() - offset].trim_start();
327             let mut tokens = tokenize(src);
328             return src[..tokens.next().unwrap().len]
329                 .to_ascii_uppercase()
330                 .contains("SAFETY:")
331                 && tokens.all(|t| t.kind == TokenKind::Whitespace);
332         }
333         match lines.next() {
334             Some(x) => (line_start, line) = x,
335             None => return false,
336         }
337     }
338 }