]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/undocumented_unsafe_blocks.rs
Auto merge of #8763 - arieluy:manual_range_contains, r=xFrednet
[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::{Block, BlockCheckMode, UnsafeSource};
6 use rustc_lexer::{tokenize, TokenKind};
7 use rustc_lint::{LateContext, LateLintPass, LintContext};
8 use rustc_middle::lint::in_external_macro;
9 use rustc_session::{declare_lint_pass, declare_tool_lint};
10 use rustc_span::{BytePos, Pos, SyntaxContext};
11
12 declare_clippy_lint! {
13     /// ### What it does
14     /// Checks for `unsafe` blocks without a `// SAFETY: ` comment
15     /// explaining why the unsafe operations performed inside
16     /// the block are safe.
17     ///
18     /// Note the comment must appear on the line(s) preceding the unsafe block
19     /// with nothing appearing in between. The following is ok:
20     /// ```ignore
21     /// foo(
22     ///     // SAFETY:
23     ///     // This is a valid safety comment
24     ///     unsafe { *x }
25     /// )
26     /// ```
27     /// But neither of these are:
28     /// ```ignore
29     /// // SAFETY:
30     /// // This is not a valid safety comment
31     /// foo(
32     ///     /* SAFETY: Neither is this */ unsafe { *x },
33     /// );
34     /// ```
35     ///
36     /// ### Why is this bad?
37     /// Undocumented unsafe blocks can make it difficult to
38     /// read and maintain code, as well as uncover unsoundness
39     /// and bugs.
40     ///
41     /// ### Example
42     /// ```rust
43     /// use std::ptr::NonNull;
44     /// let a = &mut 42;
45     ///
46     /// let ptr = unsafe { NonNull::new_unchecked(a) };
47     /// ```
48     /// Use instead:
49     /// ```rust
50     /// use std::ptr::NonNull;
51     /// let a = &mut 42;
52     ///
53     /// // SAFETY: references are guaranteed to be non-null.
54     /// let ptr = unsafe { NonNull::new_unchecked(a) };
55     /// ```
56     #[clippy::version = "1.58.0"]
57     pub UNDOCUMENTED_UNSAFE_BLOCKS,
58     restriction,
59     "creating an unsafe block without explaining why it is safe"
60 }
61
62 declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
63
64 impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
65     fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
66         if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
67             && !in_external_macro(cx.tcx.sess, block.span)
68             && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id)
69             && !is_unsafe_from_proc_macro(cx, block)
70             && !block_has_safety_comment(cx, block)
71         {
72             let source_map = cx.tcx.sess.source_map();
73             let span = if source_map.is_multiline(block.span) {
74                 source_map.span_until_char(block.span, '\n')
75             } else {
76                 block.span
77             };
78
79             span_lint_and_help(
80                 cx,
81                 UNDOCUMENTED_UNSAFE_BLOCKS,
82                 span,
83                 "unsafe block missing a safety comment",
84                 None,
85                 "consider adding a safety comment on the preceding line",
86             );
87         }
88     }
89 }
90
91 fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
92     let source_map = cx.sess().source_map();
93     let file_pos = source_map.lookup_byte_offset(block.span.lo());
94     file_pos
95         .sf
96         .src
97         .as_deref()
98         .and_then(|src| src.get(file_pos.pos.to_usize()..))
99         .map_or(true, |src| !src.starts_with("unsafe"))
100 }
101
102 /// Checks if the lines immediately preceding the block contain a safety comment.
103 fn block_has_safety_comment(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
104     // This intentionally ignores text before the start of a function so something like:
105     // ```
106     //     // SAFETY: reason
107     //     fn foo() { unsafe { .. } }
108     // ```
109     // won't work. This is to avoid dealing with where such a comment should be place relative to
110     // attributes and doc comments.
111
112     let source_map = cx.sess().source_map();
113     let ctxt = block.span.ctxt();
114     if ctxt != SyntaxContext::root() {
115         // From a macro expansion. Get the text from the start of the macro declaration to start of the unsafe block.
116         //     macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
117         //     ^--------------------------------------------^
118         if let Ok(unsafe_line) = source_map.lookup_line(block.span.lo())
119             && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
120             && Lrc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
121             && let Some(src) = unsafe_line.sf.src.as_deref()
122         {
123             macro_line.line < unsafe_line.line && text_has_safety_comment(
124                 src,
125                 &unsafe_line.sf.lines[macro_line.line + 1..=unsafe_line.line],
126                 unsafe_line.sf.start_pos.to_usize(),
127             )
128         } else {
129             // Problem getting source text. Pretend a comment was found.
130             true
131         }
132     } else if let Ok(unsafe_line) = source_map.lookup_line(block.span.lo())
133         && let Some(body) = cx.enclosing_body
134         && let Some(body_span) = walk_span_to_context(cx.tcx.hir().body(body).value.span, SyntaxContext::root())
135         && let Ok(body_line) = source_map.lookup_line(body_span.lo())
136         && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
137         && let Some(src) = unsafe_line.sf.src.as_deref()
138     {
139         // Get the text from the start of function body to the unsafe block.
140         //     fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
141         //              ^-------------^
142         body_line.line < unsafe_line.line && text_has_safety_comment(
143             src,
144             &unsafe_line.sf.lines[body_line.line + 1..=unsafe_line.line],
145             unsafe_line.sf.start_pos.to_usize(),
146         )
147     } else {
148         // Problem getting source text. Pretend a comment was found.
149         true
150     }
151 }
152
153 /// Checks if the given text has a safety comment for the immediately proceeding line.
154 fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> bool {
155     let mut lines = line_starts
156         .array_windows::<2>()
157         .rev()
158         .map_while(|[start, end]| {
159             let start = start.to_usize() - offset;
160             let end = end.to_usize() - offset;
161             src.get(start..end).map(|text| (start, text.trim_start()))
162         })
163         .filter(|(_, text)| !text.is_empty());
164
165     let Some((line_start, line)) = lines.next() else {
166         return false;
167     };
168     // Check for a sequence of line comments.
169     if line.starts_with("//") {
170         let mut line = line;
171         loop {
172             if line.to_ascii_uppercase().contains("SAFETY:") {
173                 return true;
174             }
175             match lines.next() {
176                 Some((_, x)) if x.starts_with("//") => line = x,
177                 _ => return false,
178             }
179         }
180     }
181     // No line comments; look for the start of a block comment.
182     // This will only find them if they are at the start of a line.
183     let (mut line_start, mut line) = (line_start, line);
184     loop {
185         if line.starts_with("/*") {
186             let src = src[line_start..line_starts.last().unwrap().to_usize() - offset].trim_start();
187             let mut tokens = tokenize(src);
188             return src[..tokens.next().unwrap().len]
189                 .to_ascii_uppercase()
190                 .contains("SAFETY:")
191                 && tokens.all(|t| t.kind == TokenKind::Whitespace);
192         }
193         match lines.next() {
194             Some(x) => (line_start, line) = x,
195             None => return false,
196         }
197     }
198 }