]>
Commit | Line | Data |
---|---|---|
487cf647 FG |
1 | use std::ops::ControlFlow; |
2 | ||
04454e1e | 3 | use clippy_utils::diagnostics::span_lint_and_help; |
04454e1e | 4 | use clippy_utils::source::walk_span_to_context; |
487cf647 | 5 | use clippy_utils::visitors::{for_each_expr_with_closures, Descend}; |
923072b8 | 6 | use clippy_utils::{get_parent_node, is_lint_allowed}; |
487cf647 | 7 | use hir::HirId; |
04454e1e | 8 | use rustc_data_structures::sync::Lrc; |
923072b8 FG |
9 | use rustc_hir as hir; |
10 | use rustc_hir::{Block, BlockCheckMode, ItemKind, Node, UnsafeSource}; | |
04454e1e FG |
11 | use rustc_lexer::{tokenize, TokenKind}; |
12 | use rustc_lint::{LateContext, LateLintPass, LintContext}; | |
3c0e092e | 13 | use rustc_middle::lint::in_external_macro; |
fe692bf9 | 14 | use rustc_session::{declare_tool_lint, impl_lint_pass}; |
923072b8 | 15 | use rustc_span::{BytePos, Pos, Span, SyntaxContext}; |
3c0e092e XL |
16 | |
17 | declare_clippy_lint! { | |
18 | /// ### What it does | |
923072b8 | 19 | /// Checks for `unsafe` blocks and impls without a `// SAFETY: ` comment |
3c0e092e XL |
20 | /// explaining why the unsafe operations performed inside |
21 | /// the block are safe. | |
22 | /// | |
04454e1e FG |
23 | /// Note the comment must appear on the line(s) preceding the unsafe block |
24 | /// with nothing appearing in between. The following is ok: | |
25 | /// ```ignore | |
26 | /// foo( | |
27 | /// // SAFETY: | |
28 | /// // This is a valid safety comment | |
29 | /// unsafe { *x } | |
30 | /// ) | |
31 | /// ``` | |
32 | /// But neither of these are: | |
33 | /// ```ignore | |
34 | /// // SAFETY: | |
35 | /// // This is not a valid safety comment | |
36 | /// foo( | |
37 | /// /* SAFETY: Neither is this */ unsafe { *x }, | |
38 | /// ); | |
39 | /// ``` | |
40 | /// | |
3c0e092e | 41 | /// ### Why is this bad? |
923072b8 | 42 | /// Undocumented unsafe blocks and impls can make it difficult to |
3c0e092e XL |
43 | /// read and maintain code, as well as uncover unsoundness |
44 | /// and bugs. | |
45 | /// | |
46 | /// ### Example | |
47 | /// ```rust | |
48 | /// use std::ptr::NonNull; | |
49 | /// let a = &mut 42; | |
50 | /// | |
51 | /// let ptr = unsafe { NonNull::new_unchecked(a) }; | |
52 | /// ``` | |
53 | /// Use instead: | |
54 | /// ```rust | |
55 | /// use std::ptr::NonNull; | |
56 | /// let a = &mut 42; | |
57 | /// | |
a2a8927a | 58 | /// // SAFETY: references are guaranteed to be non-null. |
3c0e092e XL |
59 | /// let ptr = unsafe { NonNull::new_unchecked(a) }; |
60 | /// ``` | |
a2a8927a | 61 | #[clippy::version = "1.58.0"] |
3c0e092e XL |
62 | pub UNDOCUMENTED_UNSAFE_BLOCKS, |
63 | restriction, | |
64 | "creating an unsafe block without explaining why it is safe" | |
65 | } | |
487cf647 FG |
66 | declare_clippy_lint! { |
67 | /// ### What it does | |
68 | /// Checks for `// SAFETY: ` comments on safe code. | |
69 | /// | |
70 | /// ### Why is this bad? | |
71 | /// Safe code has no safety requirements, so there is no need to | |
72 | /// describe safety invariants. | |
73 | /// | |
74 | /// ### Example | |
75 | /// ```rust | |
76 | /// use std::ptr::NonNull; | |
77 | /// let a = &mut 42; | |
78 | /// | |
79 | /// // SAFETY: references are guaranteed to be non-null. | |
80 | /// let ptr = NonNull::new(a).unwrap(); | |
81 | /// ``` | |
82 | /// Use instead: | |
83 | /// ```rust | |
84 | /// use std::ptr::NonNull; | |
85 | /// let a = &mut 42; | |
86 | /// | |
87 | /// let ptr = NonNull::new(a).unwrap(); | |
88 | /// ``` | |
89 | #[clippy::version = "1.67.0"] | |
90 | pub UNNECESSARY_SAFETY_COMMENT, | |
91 | restriction, | |
92 | "annotating safe code with a safety comment" | |
93 | } | |
3c0e092e | 94 | |
fe692bf9 FG |
95 | #[derive(Copy, Clone)] |
96 | pub struct UndocumentedUnsafeBlocks { | |
97 | accept_comment_above_statement: bool, | |
98 | accept_comment_above_attributes: bool, | |
99 | } | |
100 | ||
101 | impl UndocumentedUnsafeBlocks { | |
102 | pub fn new(accept_comment_above_statement: bool, accept_comment_above_attributes: bool) -> Self { | |
103 | Self { | |
104 | accept_comment_above_statement, | |
105 | accept_comment_above_attributes, | |
106 | } | |
107 | } | |
108 | } | |
109 | ||
110 | impl_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS, UNNECESSARY_SAFETY_COMMENT]); | |
3c0e092e | 111 | |
487cf647 FG |
112 | impl<'tcx> LateLintPass<'tcx> for UndocumentedUnsafeBlocks { |
113 | fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) { | |
04454e1e FG |
114 | if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) |
115 | && !in_external_macro(cx.tcx.sess, block.span) | |
116 | && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id) | |
923072b8 | 117 | && !is_unsafe_from_proc_macro(cx, block.span) |
487cf647 | 118 | && !block_has_safety_comment(cx, block.span) |
fe692bf9 FG |
119 | && !block_parents_have_safety_comment( |
120 | self.accept_comment_above_statement, | |
121 | self.accept_comment_above_attributes, | |
122 | cx, | |
123 | block.hir_id, | |
124 | ) | |
04454e1e FG |
125 | { |
126 | let source_map = cx.tcx.sess.source_map(); | |
127 | let span = if source_map.is_multiline(block.span) { | |
128 | source_map.span_until_char(block.span, '\n') | |
129 | } else { | |
130 | block.span | |
131 | }; | |
3c0e092e | 132 | |
04454e1e FG |
133 | span_lint_and_help( |
134 | cx, | |
135 | UNDOCUMENTED_UNSAFE_BLOCKS, | |
136 | span, | |
137 | "unsafe block missing a safety comment", | |
138 | None, | |
139 | "consider adding a safety comment on the preceding line", | |
140 | ); | |
3c0e092e | 141 | } |
487cf647 FG |
142 | |
143 | if let Some(tail) = block.expr | |
144 | && !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, tail.hir_id) | |
145 | && !in_external_macro(cx.tcx.sess, tail.span) | |
146 | && let HasSafetyComment::Yes(pos) = stmt_has_safety_comment(cx, tail.span, tail.hir_id) | |
147 | && let Some(help_span) = expr_has_unnecessary_safety_comment(cx, tail, pos) | |
148 | { | |
149 | span_lint_and_help( | |
150 | cx, | |
151 | UNNECESSARY_SAFETY_COMMENT, | |
152 | tail.span, | |
153 | "expression has unnecessary safety comment", | |
154 | Some(help_span), | |
155 | "consider removing the safety comment", | |
156 | ); | |
157 | } | |
3c0e092e | 158 | } |
923072b8 | 159 | |
487cf647 FG |
160 | fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &hir::Stmt<'tcx>) { |
161 | let ( | |
162 | hir::StmtKind::Local(&hir::Local { init: Some(expr), .. }) | |
163 | | hir::StmtKind::Expr(expr) | |
164 | | hir::StmtKind::Semi(expr) | |
165 | ) = stmt.kind else { return }; | |
166 | if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, stmt.hir_id) | |
167 | && !in_external_macro(cx.tcx.sess, stmt.span) | |
168 | && let HasSafetyComment::Yes(pos) = stmt_has_safety_comment(cx, stmt.span, stmt.hir_id) | |
169 | && let Some(help_span) = expr_has_unnecessary_safety_comment(cx, expr, pos) | |
923072b8 | 170 | { |
487cf647 FG |
171 | span_lint_and_help( |
172 | cx, | |
173 | UNNECESSARY_SAFETY_COMMENT, | |
174 | stmt.span, | |
175 | "statement has unnecessary safety comment", | |
176 | Some(help_span), | |
177 | "consider removing the safety comment", | |
178 | ); | |
179 | } | |
180 | } | |
181 | ||
182 | fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) { | |
183 | if in_external_macro(cx.tcx.sess, item.span) { | |
184 | return; | |
185 | } | |
186 | ||
187 | let mk_spans = |pos: BytePos| { | |
923072b8 | 188 | let source_map = cx.tcx.sess.source_map(); |
487cf647 FG |
189 | let span = Span::new(pos, pos, SyntaxContext::root(), None); |
190 | let help_span = source_map.span_extend_to_next_char(span, '\n', true); | |
923072b8 FG |
191 | let span = if source_map.is_multiline(item.span) { |
192 | source_map.span_until_char(item.span, '\n') | |
193 | } else { | |
194 | item.span | |
195 | }; | |
487cf647 FG |
196 | (span, help_span) |
197 | }; | |
923072b8 | 198 | |
487cf647 FG |
199 | let item_has_safety_comment = item_has_safety_comment(cx, item); |
200 | match (&item.kind, item_has_safety_comment) { | |
201 | // lint unsafe impl without safety comment | |
202 | (hir::ItemKind::Impl(impl_), HasSafetyComment::No) if impl_.unsafety == hir::Unsafety::Unsafe => { | |
203 | if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id()) | |
204 | && !is_unsafe_from_proc_macro(cx, item.span) | |
205 | { | |
206 | let source_map = cx.tcx.sess.source_map(); | |
207 | let span = if source_map.is_multiline(item.span) { | |
208 | source_map.span_until_char(item.span, '\n') | |
209 | } else { | |
210 | item.span | |
211 | }; | |
212 | ||
213 | span_lint_and_help( | |
214 | cx, | |
215 | UNDOCUMENTED_UNSAFE_BLOCKS, | |
216 | span, | |
217 | "unsafe impl missing a safety comment", | |
218 | None, | |
219 | "consider adding a safety comment on the preceding line", | |
220 | ); | |
221 | } | |
222 | }, | |
223 | // lint safe impl with unnecessary safety comment | |
224 | (hir::ItemKind::Impl(impl_), HasSafetyComment::Yes(pos)) if impl_.unsafety == hir::Unsafety::Normal => { | |
225 | if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) { | |
226 | let (span, help_span) = mk_spans(pos); | |
227 | ||
228 | span_lint_and_help( | |
229 | cx, | |
230 | UNNECESSARY_SAFETY_COMMENT, | |
231 | span, | |
232 | "impl has unnecessary safety comment", | |
233 | Some(help_span), | |
234 | "consider removing the safety comment", | |
235 | ); | |
236 | } | |
237 | }, | |
238 | (hir::ItemKind::Impl(_), _) => {}, | |
239 | // const and static items only need a safety comment if their body is an unsafe block, lint otherwise | |
240 | (&hir::ItemKind::Const(.., body) | &hir::ItemKind::Static(.., body), HasSafetyComment::Yes(pos)) => { | |
241 | if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, body.hir_id) { | |
242 | let body = cx.tcx.hir().body(body); | |
243 | if !matches!( | |
244 | body.value.kind, hir::ExprKind::Block(block, _) | |
245 | if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) | |
246 | ) { | |
247 | let (span, help_span) = mk_spans(pos); | |
248 | ||
249 | span_lint_and_help( | |
250 | cx, | |
251 | UNNECESSARY_SAFETY_COMMENT, | |
252 | span, | |
253 | &format!("{} has unnecessary safety comment", item.kind.descr()), | |
254 | Some(help_span), | |
255 | "consider removing the safety comment", | |
256 | ); | |
257 | } | |
258 | } | |
259 | }, | |
260 | // Aside from unsafe impls and consts/statics with an unsafe block, items in general | |
261 | // do not have safety invariants that need to be documented, so lint those. | |
262 | (_, HasSafetyComment::Yes(pos)) => { | |
263 | if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) { | |
264 | let (span, help_span) = mk_spans(pos); | |
265 | ||
266 | span_lint_and_help( | |
267 | cx, | |
268 | UNNECESSARY_SAFETY_COMMENT, | |
269 | span, | |
270 | &format!("{} has unnecessary safety comment", item.kind.descr()), | |
271 | Some(help_span), | |
272 | "consider removing the safety comment", | |
273 | ); | |
274 | } | |
275 | }, | |
276 | _ => (), | |
923072b8 FG |
277 | } |
278 | } | |
04454e1e | 279 | } |
3c0e092e | 280 | |
487cf647 FG |
281 | fn expr_has_unnecessary_safety_comment<'tcx>( |
282 | cx: &LateContext<'tcx>, | |
283 | expr: &'tcx hir::Expr<'tcx>, | |
284 | comment_pos: BytePos, | |
285 | ) -> Option<Span> { | |
9ffffee4 FG |
286 | if cx.tcx.hir().parent_iter(expr.hir_id).any(|(_, ref node)| { |
287 | matches!( | |
288 | node, | |
289 | Node::Block(&Block { | |
290 | rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided), | |
291 | .. | |
292 | }), | |
293 | ) | |
294 | }) { | |
295 | return None; | |
296 | } | |
297 | ||
487cf647 FG |
298 | // this should roughly be the reverse of `block_parents_have_safety_comment` |
299 | if for_each_expr_with_closures(cx, expr, |expr| match expr.kind { | |
300 | hir::ExprKind::Block( | |
301 | Block { | |
302 | rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided), | |
303 | .. | |
304 | }, | |
305 | _, | |
306 | ) => ControlFlow::Break(()), | |
307 | // statements will be handled by check_stmt itself again | |
308 | hir::ExprKind::Block(..) => ControlFlow::Continue(Descend::No), | |
309 | _ => ControlFlow::Continue(Descend::Yes), | |
310 | }) | |
311 | .is_some() | |
312 | { | |
313 | return None; | |
314 | } | |
315 | ||
316 | let source_map = cx.tcx.sess.source_map(); | |
317 | let span = Span::new(comment_pos, comment_pos, SyntaxContext::root(), None); | |
318 | let help_span = source_map.span_extend_to_next_char(span, '\n', true); | |
319 | ||
320 | Some(help_span) | |
321 | } | |
322 | ||
923072b8 | 323 | fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool { |
04454e1e | 324 | let source_map = cx.sess().source_map(); |
923072b8 | 325 | let file_pos = source_map.lookup_byte_offset(span.lo()); |
04454e1e FG |
326 | file_pos |
327 | .sf | |
328 | .src | |
329 | .as_deref() | |
330 | .and_then(|src| src.get(file_pos.pos.to_usize()..)) | |
331 | .map_or(true, |src| !src.starts_with("unsafe")) | |
3c0e092e XL |
332 | } |
333 | ||
487cf647 FG |
334 | // Checks if any parent {expression, statement, block, local, const, static} |
335 | // has a safety comment | |
fe692bf9 FG |
336 | fn block_parents_have_safety_comment( |
337 | accept_comment_above_statement: bool, | |
338 | accept_comment_above_attributes: bool, | |
339 | cx: &LateContext<'_>, | |
340 | id: hir::HirId, | |
341 | ) -> bool { | |
487cf647 FG |
342 | if let Some(node) = get_parent_node(cx.tcx, id) { |
343 | return match node { | |
fe692bf9 FG |
344 | Node::Expr(expr) => { |
345 | if let Some( | |
346 | Node::Local(hir::Local { span, .. }) | |
347 | | Node::Item(hir::Item { | |
348 | kind: hir::ItemKind::Const(..) | ItemKind::Static(..), | |
349 | span, | |
350 | .. | |
351 | }), | |
352 | ) = get_parent_node(cx.tcx, expr.hir_id) | |
353 | { | |
354 | let hir_id = match get_parent_node(cx.tcx, expr.hir_id) { | |
355 | Some(Node::Local(hir::Local { hir_id, .. })) => *hir_id, | |
356 | Some(Node::Item(hir::Item { owner_id, .. })) => { | |
357 | cx.tcx.hir().local_def_id_to_hir_id(owner_id.def_id) | |
358 | }, | |
359 | _ => unreachable!(), | |
360 | }; | |
361 | ||
362 | // if unsafe block is part of a let/const/static statement, | |
363 | // and accept_comment_above_statement is set to true | |
364 | // we accept the safety comment in the line the precedes this statement. | |
365 | accept_comment_above_statement | |
366 | && span_with_attrs_in_body_has_safety_comment( | |
367 | cx, | |
368 | *span, | |
369 | hir_id, | |
370 | accept_comment_above_attributes, | |
371 | ) | |
372 | } else { | |
373 | !is_branchy(expr) | |
374 | && span_with_attrs_in_body_has_safety_comment( | |
375 | cx, | |
376 | expr.span, | |
377 | expr.hir_id, | |
378 | accept_comment_above_attributes, | |
379 | ) | |
380 | } | |
381 | }, | |
487cf647 FG |
382 | Node::Stmt(hir::Stmt { |
383 | kind: | |
fe692bf9 FG |
384 | hir::StmtKind::Local(hir::Local { span, hir_id, .. }) |
385 | | hir::StmtKind::Expr(hir::Expr { span, hir_id, .. }) | |
386 | | hir::StmtKind::Semi(hir::Expr { span, hir_id, .. }), | |
487cf647 FG |
387 | .. |
388 | }) | |
fe692bf9 FG |
389 | | Node::Local(hir::Local { span, hir_id, .. }) => { |
390 | span_with_attrs_in_body_has_safety_comment(cx, *span, *hir_id, accept_comment_above_attributes) | |
391 | }, | |
392 | Node::Item(hir::Item { | |
487cf647 FG |
393 | kind: hir::ItemKind::Const(..) | ItemKind::Static(..), |
394 | span, | |
fe692bf9 | 395 | owner_id, |
487cf647 | 396 | .. |
fe692bf9 FG |
397 | }) => span_with_attrs_in_body_has_safety_comment( |
398 | cx, | |
399 | *span, | |
400 | cx.tcx.hir().local_def_id_to_hir_id(owner_id.def_id), | |
401 | accept_comment_above_attributes, | |
402 | ), | |
487cf647 FG |
403 | _ => false, |
404 | }; | |
405 | } | |
406 | false | |
407 | } | |
408 | ||
fe692bf9 FG |
409 | /// Extends `span` to also include its attributes, then checks if that span has a safety comment. |
410 | fn span_with_attrs_in_body_has_safety_comment( | |
411 | cx: &LateContext<'_>, | |
412 | span: Span, | |
413 | hir_id: HirId, | |
414 | accept_comment_above_attributes: bool, | |
415 | ) -> bool { | |
416 | let span = if accept_comment_above_attributes { | |
417 | include_attrs_in_span(cx, hir_id, span) | |
418 | } else { | |
419 | span | |
420 | }; | |
421 | ||
422 | span_in_body_has_safety_comment(cx, span) | |
423 | } | |
424 | ||
487cf647 FG |
425 | /// Checks if an expression is "branchy", e.g. loop, match/if/etc. |
426 | fn is_branchy(expr: &hir::Expr<'_>) -> bool { | |
427 | matches!( | |
428 | expr.kind, | |
429 | hir::ExprKind::If(..) | hir::ExprKind::Loop(..) | hir::ExprKind::Match(..) | |
430 | ) | |
431 | } | |
432 | ||
04454e1e | 433 | /// Checks if the lines immediately preceding the block contain a safety comment. |
487cf647 | 434 | fn block_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool { |
04454e1e FG |
435 | // This intentionally ignores text before the start of a function so something like: |
436 | // ``` | |
437 | // // SAFETY: reason | |
438 | // fn foo() { unsafe { .. } } | |
439 | // ``` | |
440 | // won't work. This is to avoid dealing with where such a comment should be place relative to | |
441 | // attributes and doc comments. | |
442 | ||
487cf647 FG |
443 | matches!( |
444 | span_from_macro_expansion_has_safety_comment(cx, span), | |
445 | HasSafetyComment::Yes(_) | |
446 | ) || span_in_body_has_safety_comment(cx, span) | |
447 | } | |
448 | ||
fe692bf9 FG |
449 | fn include_attrs_in_span(cx: &LateContext<'_>, hir_id: HirId, span: Span) -> Span { |
450 | span.to(cx | |
451 | .tcx | |
452 | .hir() | |
453 | .attrs(hir_id) | |
454 | .iter() | |
455 | .fold(span, |acc, attr| acc.to(attr.span))) | |
456 | } | |
457 | ||
487cf647 FG |
458 | enum HasSafetyComment { |
459 | Yes(BytePos), | |
460 | No, | |
461 | Maybe, | |
923072b8 FG |
462 | } |
463 | ||
464 | /// Checks if the lines immediately preceding the item contain a safety comment. | |
465 | #[allow(clippy::collapsible_match)] | |
487cf647 FG |
466 | fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> HasSafetyComment { |
467 | match span_from_macro_expansion_has_safety_comment(cx, item.span) { | |
468 | HasSafetyComment::Maybe => (), | |
469 | has_safety_comment => return has_safety_comment, | |
923072b8 FG |
470 | } |
471 | ||
487cf647 FG |
472 | if item.span.ctxt() != SyntaxContext::root() { |
473 | return HasSafetyComment::No; | |
474 | } | |
475 | if let Some(parent_node) = get_parent_node(cx.tcx, item.hir_id()) { | |
476 | let comment_start = match parent_node { | |
477 | Node::Crate(parent_mod) => { | |
478 | comment_start_before_item_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item) | |
479 | }, | |
480 | Node::Item(parent_item) => { | |
481 | if let ItemKind::Mod(parent_mod) = &parent_item.kind { | |
482 | comment_start_before_item_in_mod(cx, parent_mod, parent_item.span, item) | |
483 | } else { | |
923072b8 | 484 | // Doesn't support impls in this position. Pretend a comment was found. |
487cf647 FG |
485 | return HasSafetyComment::Maybe; |
486 | } | |
487 | }, | |
488 | Node::Stmt(stmt) => { | |
489 | if let Some(Node::Block(block)) = get_parent_node(cx.tcx, stmt.hir_id) { | |
490 | walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo) | |
491 | } else { | |
492 | // Problem getting the parent node. Pretend a comment was found. | |
493 | return HasSafetyComment::Maybe; | |
494 | } | |
495 | }, | |
496 | _ => { | |
497 | // Doesn't support impls in this position. Pretend a comment was found. | |
498 | return HasSafetyComment::Maybe; | |
499 | }, | |
500 | }; | |
923072b8 | 501 | |
487cf647 FG |
502 | let source_map = cx.sess().source_map(); |
503 | if let Some(comment_start) = comment_start | |
504 | && let Ok(unsafe_line) = source_map.lookup_line(item.span.lo()) | |
505 | && let Ok(comment_start_line) = source_map.lookup_line(comment_start) | |
506 | && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf) | |
507 | && let Some(src) = unsafe_line.sf.src.as_deref() | |
508 | { | |
509 | return unsafe_line.sf.lines(|lines| { | |
510 | if comment_start_line.line >= unsafe_line.line { | |
511 | HasSafetyComment::No | |
512 | } else { | |
513 | match text_has_safety_comment( | |
923072b8 FG |
514 | src, |
515 | &lines[comment_start_line.line + 1..=unsafe_line.line], | |
516 | unsafe_line.sf.start_pos.to_usize(), | |
487cf647 FG |
517 | ) { |
518 | Some(b) => HasSafetyComment::Yes(b), | |
519 | None => HasSafetyComment::No, | |
520 | } | |
521 | } | |
522 | }); | |
523 | } | |
524 | } | |
525 | HasSafetyComment::Maybe | |
526 | } | |
527 | ||
528 | /// Checks if the lines immediately preceding the item contain a safety comment. | |
529 | #[allow(clippy::collapsible_match)] | |
530 | fn stmt_has_safety_comment(cx: &LateContext<'_>, span: Span, hir_id: HirId) -> HasSafetyComment { | |
531 | match span_from_macro_expansion_has_safety_comment(cx, span) { | |
532 | HasSafetyComment::Maybe => (), | |
533 | has_safety_comment => return has_safety_comment, | |
534 | } | |
535 | ||
536 | if span.ctxt() != SyntaxContext::root() { | |
537 | return HasSafetyComment::No; | |
538 | } | |
539 | ||
540 | if let Some(parent_node) = get_parent_node(cx.tcx, hir_id) { | |
541 | let comment_start = match parent_node { | |
542 | Node::Block(block) => walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo), | |
543 | _ => return HasSafetyComment::Maybe, | |
544 | }; | |
545 | ||
546 | let source_map = cx.sess().source_map(); | |
547 | if let Some(comment_start) = comment_start | |
548 | && let Ok(unsafe_line) = source_map.lookup_line(span.lo()) | |
549 | && let Ok(comment_start_line) = source_map.lookup_line(comment_start) | |
550 | && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf) | |
551 | && let Some(src) = unsafe_line.sf.src.as_deref() | |
552 | { | |
553 | return unsafe_line.sf.lines(|lines| { | |
554 | if comment_start_line.line >= unsafe_line.line { | |
555 | HasSafetyComment::No | |
556 | } else { | |
557 | match text_has_safety_comment( | |
558 | src, | |
559 | &lines[comment_start_line.line + 1..=unsafe_line.line], | |
560 | unsafe_line.sf.start_pos.to_usize(), | |
561 | ) { | |
562 | Some(b) => HasSafetyComment::Yes(b), | |
563 | None => HasSafetyComment::No, | |
564 | } | |
565 | } | |
566 | }); | |
923072b8 | 567 | } |
923072b8 | 568 | } |
487cf647 | 569 | HasSafetyComment::Maybe |
923072b8 FG |
570 | } |
571 | ||
487cf647 | 572 | fn comment_start_before_item_in_mod( |
923072b8 FG |
573 | cx: &LateContext<'_>, |
574 | parent_mod: &hir::Mod<'_>, | |
575 | parent_mod_span: Span, | |
487cf647 | 576 | item: &hir::Item<'_>, |
923072b8 FG |
577 | ) -> Option<BytePos> { |
578 | parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| { | |
487cf647 | 579 | if *item_id == item.item_id() { |
923072b8 FG |
580 | if idx == 0 { |
581 | // mod A { /* comment */ unsafe impl T {} ... } | |
582 | // ^------------------------------------------^ returns the start of this span | |
583 | // ^---------------------^ finally checks comments in this range | |
584 | if let Some(sp) = walk_span_to_context(parent_mod_span, SyntaxContext::root()) { | |
585 | return Some(sp.lo()); | |
586 | } | |
587 | } else { | |
588 | // some_item /* comment */ unsafe impl T {} | |
589 | // ^-------^ returns the end of this span | |
590 | // ^---------------^ finally checks comments in this range | |
591 | let prev_item = cx.tcx.hir().item(parent_mod.item_ids[idx - 1]); | |
592 | if let Some(sp) = walk_span_to_context(prev_item.span, SyntaxContext::root()) { | |
593 | return Some(sp.hi()); | |
594 | } | |
595 | } | |
596 | } | |
597 | None | |
598 | }) | |
599 | } | |
600 | ||
487cf647 | 601 | fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> HasSafetyComment { |
04454e1e | 602 | let source_map = cx.sess().source_map(); |
923072b8 FG |
603 | let ctxt = span.ctxt(); |
604 | if ctxt == SyntaxContext::root() { | |
487cf647 | 605 | HasSafetyComment::Maybe |
923072b8 FG |
606 | } else { |
607 | // From a macro expansion. Get the text from the start of the macro declaration to start of the | |
608 | // unsafe block. | |
04454e1e FG |
609 | // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; } |
610 | // ^--------------------------------------------^ | |
923072b8 | 611 | if let Ok(unsafe_line) = source_map.lookup_line(span.lo()) |
04454e1e FG |
612 | && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo()) |
613 | && Lrc::ptr_eq(&unsafe_line.sf, ¯o_line.sf) | |
614 | && let Some(src) = unsafe_line.sf.src.as_deref() | |
615 | { | |
923072b8 | 616 | unsafe_line.sf.lines(|lines| { |
487cf647 FG |
617 | if macro_line.line < unsafe_line.line { |
618 | match text_has_safety_comment( | |
619 | src, | |
620 | &lines[macro_line.line + 1..=unsafe_line.line], | |
621 | unsafe_line.sf.start_pos.to_usize(), | |
622 | ) { | |
623 | Some(b) => HasSafetyComment::Yes(b), | |
624 | None => HasSafetyComment::No, | |
625 | } | |
626 | } else { | |
627 | HasSafetyComment::No | |
628 | } | |
923072b8 | 629 | }) |
04454e1e FG |
630 | } else { |
631 | // Problem getting source text. Pretend a comment was found. | |
487cf647 | 632 | HasSafetyComment::Maybe |
3c0e092e | 633 | } |
923072b8 FG |
634 | } |
635 | } | |
636 | ||
064997fb FG |
637 | fn get_body_search_span(cx: &LateContext<'_>) -> Option<Span> { |
638 | let body = cx.enclosing_body?; | |
639 | let map = cx.tcx.hir(); | |
640 | let mut span = map.body(body).value.span; | |
641 | for (_, node) in map.parent_iter(body.hir_id) { | |
642 | match node { | |
643 | Node::Expr(e) => span = e.span, | |
fe692bf9 FG |
644 | Node::Block(_) |
645 | | Node::Arm(_) | |
646 | | Node::Stmt(_) | |
647 | | Node::Local(_) | |
648 | | Node::Item(hir::Item { | |
649 | kind: hir::ItemKind::Const(..) | ItemKind::Static(..), | |
650 | .. | |
651 | }) => (), | |
064997fb FG |
652 | _ => break, |
653 | } | |
654 | } | |
655 | Some(span) | |
656 | } | |
657 | ||
923072b8 FG |
658 | fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool { |
659 | let source_map = cx.sess().source_map(); | |
660 | let ctxt = span.ctxt(); | |
661 | if ctxt == SyntaxContext::root() | |
064997fb | 662 | && let Some(search_span) = get_body_search_span(cx) |
04454e1e | 663 | { |
923072b8 | 664 | if let Ok(unsafe_line) = source_map.lookup_line(span.lo()) |
064997fb | 665 | && let Some(body_span) = walk_span_to_context(search_span, SyntaxContext::root()) |
923072b8 FG |
666 | && let Ok(body_line) = source_map.lookup_line(body_span.lo()) |
667 | && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf) | |
668 | && let Some(src) = unsafe_line.sf.src.as_deref() | |
669 | { | |
670 | // Get the text from the start of function body to the unsafe block. | |
671 | // fn foo() { some_stuff; unsafe { stuff }; other_stuff; } | |
672 | // ^-------------^ | |
673 | unsafe_line.sf.lines(|lines| { | |
674 | body_line.line < unsafe_line.line && text_has_safety_comment( | |
675 | src, | |
676 | &lines[body_line.line + 1..=unsafe_line.line], | |
677 | unsafe_line.sf.start_pos.to_usize(), | |
487cf647 | 678 | ).is_some() |
923072b8 FG |
679 | }) |
680 | } else { | |
681 | // Problem getting source text. Pretend a comment was found. | |
682 | true | |
683 | } | |
04454e1e | 684 | } else { |
923072b8 | 685 | false |
3c0e092e XL |
686 | } |
687 | } | |
688 | ||
04454e1e | 689 | /// Checks if the given text has a safety comment for the immediately proceeding line. |
487cf647 | 690 | fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> Option<BytePos> { |
04454e1e FG |
691 | let mut lines = line_starts |
692 | .array_windows::<2>() | |
693 | .rev() | |
694 | .map_while(|[start, end]| { | |
695 | let start = start.to_usize() - offset; | |
696 | let end = end.to_usize() - offset; | |
487cf647 FG |
697 | let text = src.get(start..end)?; |
698 | let trimmed = text.trim_start(); | |
699 | Some((start + (text.len() - trimmed.len()), trimmed)) | |
04454e1e FG |
700 | }) |
701 | .filter(|(_, text)| !text.is_empty()); | |
702 | ||
703 | let Some((line_start, line)) = lines.next() else { | |
487cf647 | 704 | return None; |
04454e1e FG |
705 | }; |
706 | // Check for a sequence of line comments. | |
707 | if line.starts_with("//") { | |
487cf647 | 708 | let (mut line, mut line_start) = (line, line_start); |
04454e1e FG |
709 | loop { |
710 | if line.to_ascii_uppercase().contains("SAFETY:") { | |
487cf647 FG |
711 | return Some(BytePos( |
712 | u32::try_from(line_start).unwrap() + u32::try_from(offset).unwrap(), | |
713 | )); | |
04454e1e FG |
714 | } |
715 | match lines.next() { | |
487cf647 FG |
716 | Some((s, x)) if x.starts_with("//") => (line, line_start) = (x, s), |
717 | _ => return None, | |
3c0e092e | 718 | } |
3c0e092e | 719 | } |
3c0e092e | 720 | } |
04454e1e FG |
721 | // No line comments; look for the start of a block comment. |
722 | // This will only find them if they are at the start of a line. | |
723 | let (mut line_start, mut line) = (line_start, line); | |
724 | loop { | |
725 | if line.starts_with("/*") { | |
487cf647 | 726 | let src = &src[line_start..line_starts.last().unwrap().to_usize() - offset]; |
04454e1e | 727 | let mut tokens = tokenize(src); |
487cf647 | 728 | return (src[..tokens.next().unwrap().len as usize] |
04454e1e FG |
729 | .to_ascii_uppercase() |
730 | .contains("SAFETY:") | |
487cf647 FG |
731 | && tokens.all(|t| t.kind == TokenKind::Whitespace)) |
732 | .then_some(BytePos( | |
733 | u32::try_from(line_start).unwrap() + u32::try_from(offset).unwrap(), | |
734 | )); | |
3c0e092e | 735 | } |
04454e1e FG |
736 | match lines.next() { |
737 | Some(x) => (line_start, line) = x, | |
487cf647 | 738 | None => return None, |
3c0e092e XL |
739 | } |
740 | } | |
741 | } |