]>
Commit | Line | Data |
---|---|---|
04454e1e | 1 | use clippy_utils::diagnostics::span_lint_and_help; |
04454e1e | 2 | use clippy_utils::source::walk_span_to_context; |
923072b8 | 3 | use clippy_utils::{get_parent_node, is_lint_allowed}; |
04454e1e | 4 | use rustc_data_structures::sync::Lrc; |
923072b8 FG |
5 | use rustc_hir as hir; |
6 | use rustc_hir::{Block, BlockCheckMode, ItemKind, Node, UnsafeSource}; | |
04454e1e FG |
7 | use rustc_lexer::{tokenize, TokenKind}; |
8 | use rustc_lint::{LateContext, LateLintPass, LintContext}; | |
3c0e092e | 9 | use rustc_middle::lint::in_external_macro; |
04454e1e | 10 | use rustc_session::{declare_lint_pass, declare_tool_lint}; |
923072b8 | 11 | use rustc_span::{BytePos, Pos, Span, SyntaxContext}; |
3c0e092e XL |
12 | |
13 | declare_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 | 63 | declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]); |
3c0e092e XL |
64 | |
65 | impl 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 | 118 | fn 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 | 130 | fn 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)] | |
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 | 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 | ||
210 | fn 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 | ||
239 | fn 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, ¯o_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 | ||
268 | fn 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. |
300 | fn 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 | } |