]> git.proxmox.com Git - rustc.git/blame - src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs
New upstream version 1.63.0+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / undocumented_unsafe_blocks.rs
CommitLineData
04454e1e 1use clippy_utils::diagnostics::span_lint_and_help;
04454e1e 2use clippy_utils::source::walk_span_to_context;
923072b8 3use clippy_utils::{get_parent_node, is_lint_allowed};
04454e1e 4use rustc_data_structures::sync::Lrc;
923072b8
FG
5use rustc_hir as hir;
6use rustc_hir::{Block, BlockCheckMode, ItemKind, Node, UnsafeSource};
04454e1e
FG
7use rustc_lexer::{tokenize, TokenKind};
8use rustc_lint::{LateContext, LateLintPass, LintContext};
3c0e092e 9use rustc_middle::lint::in_external_macro;
04454e1e 10use rustc_session::{declare_lint_pass, declare_tool_lint};
923072b8 11use rustc_span::{BytePos, Pos, Span, SyntaxContext};
3c0e092e
XL
12
13declare_clippy_lint! {
14 /// ### What it does
923072b8 15 /// Checks for `unsafe` blocks and impls without a `// SAFETY: ` comment
3c0e092e
XL
16 /// explaining why the unsafe operations performed inside
17 /// the block are safe.
18 ///
04454e1e
FG
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 ///
3c0e092e 37 /// ### Why is this bad?
923072b8 38 /// Undocumented unsafe blocks and impls can make it difficult to
3c0e092e
XL
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 ///
a2a8927a 54 /// // SAFETY: references are guaranteed to be non-null.
3c0e092e
XL
55 /// let ptr = unsafe { NonNull::new_unchecked(a) };
56 /// ```
a2a8927a 57 #[clippy::version = "1.58.0"]
3c0e092e
XL
58 pub UNDOCUMENTED_UNSAFE_BLOCKS,
59 restriction,
60 "creating an unsafe block without explaining why it is safe"
61}
62
04454e1e 63declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]);
3c0e092e
XL
64
65impl LateLintPass<'_> for UndocumentedUnsafeBlocks {
66 fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) {
04454e1e
FG
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)
923072b8 70 && !is_unsafe_from_proc_macro(cx, block.span)
04454e1e
FG
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 };
3c0e092e 79
04454e1e
FG
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 );
3c0e092e
XL
88 }
89 }
923072b8
FG
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 }
04454e1e 116}
3c0e092e 117
923072b8 118fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool {
04454e1e 119 let source_map = cx.sess().source_map();
923072b8 120 let file_pos = source_map.lookup_byte_offset(span.lo());
04454e1e
FG
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"))
3c0e092e
XL
127}
128
04454e1e 129/// Checks if the lines immediately preceding the block contain a safety comment.
923072b8 130fn block_has_safety_comment(cx: &LateContext<'_>, block: &hir::Block<'_>) -> bool {
04454e1e
FG
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
923072b8
FG
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)]
144fn 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 unsafe_line.sf.lines(|lines| {
191 comment_start_line.line < unsafe_line.line && text_has_safety_comment(
192 src,
193 &lines[comment_start_line.line + 1..=unsafe_line.line],
194 unsafe_line.sf.start_pos.to_usize(),
195 )
196 })
197 } else {
198 // Problem getting source text. Pretend a comment was found.
199 true
200 }
201 } else {
202 // No parent node. Pretend a comment was found.
203 true
204 }
205 } else {
206 false
207 }
208}
209
210fn comment_start_before_impl_in_mod(
211 cx: &LateContext<'_>,
212 parent_mod: &hir::Mod<'_>,
213 parent_mod_span: Span,
214 imple: &hir::Item<'_>,
215) -> Option<BytePos> {
216 parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| {
217 if *item_id == imple.item_id() {
218 if idx == 0 {
219 // mod A { /* comment */ unsafe impl T {} ... }
220 // ^------------------------------------------^ returns the start of this span
221 // ^---------------------^ finally checks comments in this range
222 if let Some(sp) = walk_span_to_context(parent_mod_span, SyntaxContext::root()) {
223 return Some(sp.lo());
224 }
225 } else {
226 // some_item /* comment */ unsafe impl T {}
227 // ^-------^ returns the end of this span
228 // ^---------------^ finally checks comments in this range
229 let prev_item = cx.tcx.hir().item(parent_mod.item_ids[idx - 1]);
230 if let Some(sp) = walk_span_to_context(prev_item.span, SyntaxContext::root()) {
231 return Some(sp.hi());
232 }
233 }
234 }
235 None
236 })
237}
238
239fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
04454e1e 240 let source_map = cx.sess().source_map();
923072b8
FG
241 let ctxt = span.ctxt();
242 if ctxt == SyntaxContext::root() {
243 false
244 } else {
245 // From a macro expansion. Get the text from the start of the macro declaration to start of the
246 // unsafe block.
04454e1e
FG
247 // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
248 // ^--------------------------------------------^
923072b8 249 if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
04454e1e
FG
250 && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
251 && Lrc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
252 && let Some(src) = unsafe_line.sf.src.as_deref()
253 {
923072b8
FG
254 unsafe_line.sf.lines(|lines| {
255 macro_line.line < unsafe_line.line && text_has_safety_comment(
256 src,
257 &lines[macro_line.line + 1..=unsafe_line.line],
258 unsafe_line.sf.start_pos.to_usize(),
259 )
260 })
04454e1e
FG
261 } else {
262 // Problem getting source text. Pretend a comment was found.
263 true
3c0e092e 264 }
923072b8
FG
265 }
266}
267
268fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
269 let source_map = cx.sess().source_map();
270 let ctxt = span.ctxt();
271 if ctxt == SyntaxContext::root()
04454e1e 272 && let Some(body) = cx.enclosing_body
04454e1e 273 {
923072b8
FG
274 if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
275 && let Some(body_span) = walk_span_to_context(cx.tcx.hir().body(body).value.span, SyntaxContext::root())
276 && let Ok(body_line) = source_map.lookup_line(body_span.lo())
277 && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
278 && let Some(src) = unsafe_line.sf.src.as_deref()
279 {
280 // Get the text from the start of function body to the unsafe block.
281 // fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
282 // ^-------------^
283 unsafe_line.sf.lines(|lines| {
284 body_line.line < unsafe_line.line && text_has_safety_comment(
285 src,
286 &lines[body_line.line + 1..=unsafe_line.line],
287 unsafe_line.sf.start_pos.to_usize(),
288 )
289 })
290 } else {
291 // Problem getting source text. Pretend a comment was found.
292 true
293 }
04454e1e 294 } else {
923072b8 295 false
3c0e092e
XL
296 }
297}
298
04454e1e
FG
299/// Checks if the given text has a safety comment for the immediately proceeding line.
300fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> bool {
301 let mut lines = line_starts
302 .array_windows::<2>()
303 .rev()
304 .map_while(|[start, end]| {
305 let start = start.to_usize() - offset;
306 let end = end.to_usize() - offset;
307 src.get(start..end).map(|text| (start, text.trim_start()))
308 })
309 .filter(|(_, text)| !text.is_empty());
310
311 let Some((line_start, line)) = lines.next() else {
312 return false;
313 };
314 // Check for a sequence of line comments.
315 if line.starts_with("//") {
316 let mut line = line;
317 loop {
318 if line.to_ascii_uppercase().contains("SAFETY:") {
319 return true;
320 }
321 match lines.next() {
322 Some((_, x)) if x.starts_with("//") => line = x,
323 _ => return false,
3c0e092e 324 }
3c0e092e 325 }
3c0e092e 326 }
04454e1e
FG
327 // No line comments; look for the start of a block comment.
328 // This will only find them if they are at the start of a line.
329 let (mut line_start, mut line) = (line_start, line);
330 loop {
331 if line.starts_with("/*") {
332 let src = src[line_start..line_starts.last().unwrap().to_usize() - offset].trim_start();
333 let mut tokens = tokenize(src);
334 return src[..tokens.next().unwrap().len]
335 .to_ascii_uppercase()
336 .contains("SAFETY:")
337 && tokens.all(|t| t.kind == TokenKind::Whitespace);
3c0e092e 338 }
04454e1e
FG
339 match lines.next() {
340 Some(x) => (line_start, line) = x,
341 None => return false,
3c0e092e
XL
342 }
343 }
344}