]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs
Rollup merge of #106260 - chenyukang:yukang/fix-106213-doc, r=GuillaumeGomez
[rust.git] / src / tools / clippy / clippy_lints / src / undocumented_unsafe_blocks.rs
1 use std::ops::ControlFlow;
2
3 use clippy_utils::diagnostics::span_lint_and_help;
4 use clippy_utils::source::walk_span_to_context;
5 use clippy_utils::visitors::{for_each_expr_with_closures, Descend};
6 use clippy_utils::{get_parent_node, is_lint_allowed};
7 use hir::HirId;
8 use rustc_data_structures::sync::Lrc;
9 use rustc_hir as hir;
10 use rustc_hir::{Block, BlockCheckMode, ItemKind, Node, UnsafeSource};
11 use rustc_lexer::{tokenize, TokenKind};
12 use rustc_lint::{LateContext, LateLintPass, LintContext};
13 use rustc_middle::lint::in_external_macro;
14 use rustc_session::{declare_lint_pass, declare_tool_lint};
15 use rustc_span::{BytePos, Pos, Span, SyntaxContext};
16
17 declare_clippy_lint! {
18     /// ### What it does
19     /// Checks for `unsafe` blocks and impls without a `// SAFETY: ` comment
20     /// explaining why the unsafe operations performed inside
21     /// the block are safe.
22     ///
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     ///
41     /// ### Why is this bad?
42     /// Undocumented unsafe blocks and impls can make it difficult to
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     ///
58     /// // SAFETY: references are guaranteed to be non-null.
59     /// let ptr = unsafe { NonNull::new_unchecked(a) };
60     /// ```
61     #[clippy::version = "1.58.0"]
62     pub UNDOCUMENTED_UNSAFE_BLOCKS,
63     restriction,
64     "creating an unsafe block without explaining why it is safe"
65 }
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 }
94
95 declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS, UNNECESSARY_SAFETY_COMMENT]);
96
97 impl<'tcx> LateLintPass<'tcx> for UndocumentedUnsafeBlocks {
98     fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) {
99         if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
100             && !in_external_macro(cx.tcx.sess, block.span)
101             && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id)
102             && !is_unsafe_from_proc_macro(cx, block.span)
103             && !block_has_safety_comment(cx, block.span)
104             && !block_parents_have_safety_comment(cx, block.hir_id)
105         {
106             let source_map = cx.tcx.sess.source_map();
107             let span = if source_map.is_multiline(block.span) {
108                 source_map.span_until_char(block.span, '\n')
109             } else {
110                 block.span
111             };
112
113             span_lint_and_help(
114                 cx,
115                 UNDOCUMENTED_UNSAFE_BLOCKS,
116                 span,
117                 "unsafe block missing a safety comment",
118                 None,
119                 "consider adding a safety comment on the preceding line",
120             );
121         }
122
123         if let Some(tail) = block.expr
124             && !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, tail.hir_id)
125             && !in_external_macro(cx.tcx.sess, tail.span)
126             && let HasSafetyComment::Yes(pos) = stmt_has_safety_comment(cx, tail.span, tail.hir_id)
127             && let Some(help_span) = expr_has_unnecessary_safety_comment(cx, tail, pos)
128         {
129             span_lint_and_help(
130                 cx,
131                 UNNECESSARY_SAFETY_COMMENT,
132                 tail.span,
133                 "expression has unnecessary safety comment",
134                 Some(help_span),
135                 "consider removing the safety comment",
136             );
137         }
138     }
139
140     fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &hir::Stmt<'tcx>) {
141         let (
142             hir::StmtKind::Local(&hir::Local { init: Some(expr), .. })
143             | hir::StmtKind::Expr(expr)
144             | hir::StmtKind::Semi(expr)
145         ) = stmt.kind else { return };
146         if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, stmt.hir_id)
147             && !in_external_macro(cx.tcx.sess, stmt.span)
148             && let HasSafetyComment::Yes(pos) = stmt_has_safety_comment(cx, stmt.span, stmt.hir_id)
149             && let Some(help_span) = expr_has_unnecessary_safety_comment(cx, expr, pos)
150         {
151             span_lint_and_help(
152                 cx,
153                 UNNECESSARY_SAFETY_COMMENT,
154                 stmt.span,
155                 "statement has unnecessary safety comment",
156                 Some(help_span),
157                 "consider removing the safety comment",
158             );
159         }
160     }
161
162     fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) {
163         if in_external_macro(cx.tcx.sess, item.span) {
164             return;
165         }
166
167         let mk_spans = |pos: BytePos| {
168             let source_map = cx.tcx.sess.source_map();
169             let span = Span::new(pos, pos, SyntaxContext::root(), None);
170             let help_span = source_map.span_extend_to_next_char(span, '\n', true);
171             let span = if source_map.is_multiline(item.span) {
172                 source_map.span_until_char(item.span, '\n')
173             } else {
174                 item.span
175             };
176             (span, help_span)
177         };
178
179         let item_has_safety_comment = item_has_safety_comment(cx, item);
180         match (&item.kind, item_has_safety_comment) {
181             // lint unsafe impl without safety comment
182             (hir::ItemKind::Impl(impl_), HasSafetyComment::No) if impl_.unsafety == hir::Unsafety::Unsafe => {
183                 if !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id())
184                     && !is_unsafe_from_proc_macro(cx, item.span)
185                 {
186                     let source_map = cx.tcx.sess.source_map();
187                     let span = if source_map.is_multiline(item.span) {
188                         source_map.span_until_char(item.span, '\n')
189                     } else {
190                         item.span
191                     };
192
193                     span_lint_and_help(
194                         cx,
195                         UNDOCUMENTED_UNSAFE_BLOCKS,
196                         span,
197                         "unsafe impl missing a safety comment",
198                         None,
199                         "consider adding a safety comment on the preceding line",
200                     );
201                 }
202             },
203             // lint safe impl with unnecessary safety comment
204             (hir::ItemKind::Impl(impl_), HasSafetyComment::Yes(pos)) if impl_.unsafety == hir::Unsafety::Normal => {
205                 if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) {
206                     let (span, help_span) = mk_spans(pos);
207
208                     span_lint_and_help(
209                         cx,
210                         UNNECESSARY_SAFETY_COMMENT,
211                         span,
212                         "impl has unnecessary safety comment",
213                         Some(help_span),
214                         "consider removing the safety comment",
215                     );
216                 }
217             },
218             (hir::ItemKind::Impl(_), _) => {},
219             // const and static items only need a safety comment if their body is an unsafe block, lint otherwise
220             (&hir::ItemKind::Const(.., body) | &hir::ItemKind::Static(.., body), HasSafetyComment::Yes(pos)) => {
221                 if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, body.hir_id) {
222                     let body = cx.tcx.hir().body(body);
223                     if !matches!(
224                         body.value.kind, hir::ExprKind::Block(block, _)
225                         if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
226                     ) {
227                         let (span, help_span) = mk_spans(pos);
228
229                         span_lint_and_help(
230                             cx,
231                             UNNECESSARY_SAFETY_COMMENT,
232                             span,
233                             &format!("{} has unnecessary safety comment", item.kind.descr()),
234                             Some(help_span),
235                             "consider removing the safety comment",
236                         );
237                     }
238                 }
239             },
240             // Aside from unsafe impls and consts/statics with an unsafe block, items in general
241             // do not have safety invariants that need to be documented, so lint those.
242             (_, HasSafetyComment::Yes(pos)) => {
243                 if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) {
244                     let (span, help_span) = mk_spans(pos);
245
246                     span_lint_and_help(
247                         cx,
248                         UNNECESSARY_SAFETY_COMMENT,
249                         span,
250                         &format!("{} has unnecessary safety comment", item.kind.descr()),
251                         Some(help_span),
252                         "consider removing the safety comment",
253                     );
254                 }
255             },
256             _ => (),
257         }
258     }
259 }
260
261 fn expr_has_unnecessary_safety_comment<'tcx>(
262     cx: &LateContext<'tcx>,
263     expr: &'tcx hir::Expr<'tcx>,
264     comment_pos: BytePos,
265 ) -> Option<Span> {
266     // this should roughly be the reverse of `block_parents_have_safety_comment`
267     if for_each_expr_with_closures(cx, expr, |expr| match expr.kind {
268         hir::ExprKind::Block(
269             Block {
270                 rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided),
271                 ..
272             },
273             _,
274         ) => ControlFlow::Break(()),
275         // statements will be handled by check_stmt itself again
276         hir::ExprKind::Block(..) => ControlFlow::Continue(Descend::No),
277         _ => ControlFlow::Continue(Descend::Yes),
278     })
279     .is_some()
280     {
281         return None;
282     }
283
284     let source_map = cx.tcx.sess.source_map();
285     let span = Span::new(comment_pos, comment_pos, SyntaxContext::root(), None);
286     let help_span = source_map.span_extend_to_next_char(span, '\n', true);
287
288     Some(help_span)
289 }
290
291 fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool {
292     let source_map = cx.sess().source_map();
293     let file_pos = source_map.lookup_byte_offset(span.lo());
294     file_pos
295         .sf
296         .src
297         .as_deref()
298         .and_then(|src| src.get(file_pos.pos.to_usize()..))
299         .map_or(true, |src| !src.starts_with("unsafe"))
300 }
301
302 // Checks if any parent {expression, statement, block, local, const, static}
303 // has a safety comment
304 fn block_parents_have_safety_comment(cx: &LateContext<'_>, id: hir::HirId) -> bool {
305     if let Some(node) = get_parent_node(cx.tcx, id) {
306         return match node {
307             Node::Expr(expr) => !is_branchy(expr) && span_in_body_has_safety_comment(cx, expr.span),
308             Node::Stmt(hir::Stmt {
309                 kind:
310                     hir::StmtKind::Local(hir::Local { span, .. })
311                     | hir::StmtKind::Expr(hir::Expr { span, .. })
312                     | hir::StmtKind::Semi(hir::Expr { span, .. }),
313                 ..
314             })
315             | Node::Local(hir::Local { span, .. })
316             | Node::Item(hir::Item {
317                 kind: hir::ItemKind::Const(..) | ItemKind::Static(..),
318                 span,
319                 ..
320             }) => span_in_body_has_safety_comment(cx, *span),
321             _ => false,
322         };
323     }
324     false
325 }
326
327 /// Checks if an expression is "branchy", e.g. loop, match/if/etc.
328 fn is_branchy(expr: &hir::Expr<'_>) -> bool {
329     matches!(
330         expr.kind,
331         hir::ExprKind::If(..) | hir::ExprKind::Loop(..) | hir::ExprKind::Match(..)
332     )
333 }
334
335 /// Checks if the lines immediately preceding the block contain a safety comment.
336 fn block_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
337     // This intentionally ignores text before the start of a function so something like:
338     // ```
339     //     // SAFETY: reason
340     //     fn foo() { unsafe { .. } }
341     // ```
342     // won't work. This is to avoid dealing with where such a comment should be place relative to
343     // attributes and doc comments.
344
345     matches!(
346         span_from_macro_expansion_has_safety_comment(cx, span),
347         HasSafetyComment::Yes(_)
348     ) || span_in_body_has_safety_comment(cx, span)
349 }
350
351 enum HasSafetyComment {
352     Yes(BytePos),
353     No,
354     Maybe,
355 }
356
357 /// Checks if the lines immediately preceding the item contain a safety comment.
358 #[allow(clippy::collapsible_match)]
359 fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> HasSafetyComment {
360     match span_from_macro_expansion_has_safety_comment(cx, item.span) {
361         HasSafetyComment::Maybe => (),
362         has_safety_comment => return has_safety_comment,
363     }
364
365     if item.span.ctxt() != SyntaxContext::root() {
366         return HasSafetyComment::No;
367     }
368     if let Some(parent_node) = get_parent_node(cx.tcx, item.hir_id()) {
369         let comment_start = match parent_node {
370             Node::Crate(parent_mod) => {
371                 comment_start_before_item_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item)
372             },
373             Node::Item(parent_item) => {
374                 if let ItemKind::Mod(parent_mod) = &parent_item.kind {
375                     comment_start_before_item_in_mod(cx, parent_mod, parent_item.span, item)
376                 } else {
377                     // Doesn't support impls in this position. Pretend a comment was found.
378                     return HasSafetyComment::Maybe;
379                 }
380             },
381             Node::Stmt(stmt) => {
382                 if let Some(Node::Block(block)) = get_parent_node(cx.tcx, stmt.hir_id) {
383                     walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo)
384                 } else {
385                     // Problem getting the parent node. Pretend a comment was found.
386                     return HasSafetyComment::Maybe;
387                 }
388             },
389             _ => {
390                 // Doesn't support impls in this position. Pretend a comment was found.
391                 return HasSafetyComment::Maybe;
392             },
393         };
394
395         let source_map = cx.sess().source_map();
396         if let Some(comment_start) = comment_start
397             && let Ok(unsafe_line) = source_map.lookup_line(item.span.lo())
398             && let Ok(comment_start_line) = source_map.lookup_line(comment_start)
399             && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf)
400             && let Some(src) = unsafe_line.sf.src.as_deref()
401         {
402             return unsafe_line.sf.lines(|lines| {
403                 if comment_start_line.line >= unsafe_line.line {
404                     HasSafetyComment::No
405                 } else {
406                     match text_has_safety_comment(
407                         src,
408                         &lines[comment_start_line.line + 1..=unsafe_line.line],
409                         unsafe_line.sf.start_pos.to_usize(),
410                     ) {
411                         Some(b) => HasSafetyComment::Yes(b),
412                         None => HasSafetyComment::No,
413                     }
414                 }
415             });
416         }
417     }
418     HasSafetyComment::Maybe
419 }
420
421 /// Checks if the lines immediately preceding the item contain a safety comment.
422 #[allow(clippy::collapsible_match)]
423 fn stmt_has_safety_comment(cx: &LateContext<'_>, span: Span, hir_id: HirId) -> HasSafetyComment {
424     match span_from_macro_expansion_has_safety_comment(cx, span) {
425         HasSafetyComment::Maybe => (),
426         has_safety_comment => return has_safety_comment,
427     }
428
429     if span.ctxt() != SyntaxContext::root() {
430         return HasSafetyComment::No;
431     }
432
433     if let Some(parent_node) = get_parent_node(cx.tcx, hir_id) {
434         let comment_start = match parent_node {
435             Node::Block(block) => walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo),
436             _ => return HasSafetyComment::Maybe,
437         };
438
439         let source_map = cx.sess().source_map();
440         if let Some(comment_start) = comment_start
441             && let Ok(unsafe_line) = source_map.lookup_line(span.lo())
442             && let Ok(comment_start_line) = source_map.lookup_line(comment_start)
443             && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf)
444             && let Some(src) = unsafe_line.sf.src.as_deref()
445         {
446             return unsafe_line.sf.lines(|lines| {
447                 if comment_start_line.line >= unsafe_line.line {
448                     HasSafetyComment::No
449                 } else {
450                     match text_has_safety_comment(
451                         src,
452                         &lines[comment_start_line.line + 1..=unsafe_line.line],
453                         unsafe_line.sf.start_pos.to_usize(),
454                     ) {
455                         Some(b) => HasSafetyComment::Yes(b),
456                         None => HasSafetyComment::No,
457                     }
458                 }
459             });
460         }
461     }
462     HasSafetyComment::Maybe
463 }
464
465 fn comment_start_before_item_in_mod(
466     cx: &LateContext<'_>,
467     parent_mod: &hir::Mod<'_>,
468     parent_mod_span: Span,
469     item: &hir::Item<'_>,
470 ) -> Option<BytePos> {
471     parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| {
472         if *item_id == item.item_id() {
473             if idx == 0 {
474                 // mod A { /* comment */ unsafe impl T {} ... }
475                 // ^------------------------------------------^ returns the start of this span
476                 // ^---------------------^ finally checks comments in this range
477                 if let Some(sp) = walk_span_to_context(parent_mod_span, SyntaxContext::root()) {
478                     return Some(sp.lo());
479                 }
480             } else {
481                 // some_item /* comment */ unsafe impl T {}
482                 // ^-------^ returns the end of this span
483                 //         ^---------------^ finally checks comments in this range
484                 let prev_item = cx.tcx.hir().item(parent_mod.item_ids[idx - 1]);
485                 if let Some(sp) = walk_span_to_context(prev_item.span, SyntaxContext::root()) {
486                     return Some(sp.hi());
487                 }
488             }
489         }
490         None
491     })
492 }
493
494 fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> HasSafetyComment {
495     let source_map = cx.sess().source_map();
496     let ctxt = span.ctxt();
497     if ctxt == SyntaxContext::root() {
498         HasSafetyComment::Maybe
499     } else {
500         // From a macro expansion. Get the text from the start of the macro declaration to start of the
501         // unsafe block.
502         //     macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
503         //     ^--------------------------------------------^
504         if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
505             && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
506             && Lrc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
507             && let Some(src) = unsafe_line.sf.src.as_deref()
508         {
509             unsafe_line.sf.lines(|lines| {
510                 if macro_line.line < unsafe_line.line {
511                     match text_has_safety_comment(
512                         src,
513                         &lines[macro_line.line + 1..=unsafe_line.line],
514                         unsafe_line.sf.start_pos.to_usize(),
515                     ) {
516                         Some(b) => HasSafetyComment::Yes(b),
517                         None => HasSafetyComment::No,
518                     }
519                 } else {
520                     HasSafetyComment::No
521                 }
522             })
523         } else {
524             // Problem getting source text. Pretend a comment was found.
525             HasSafetyComment::Maybe
526         }
527     }
528 }
529
530 fn get_body_search_span(cx: &LateContext<'_>) -> Option<Span> {
531     let body = cx.enclosing_body?;
532     let map = cx.tcx.hir();
533     let mut span = map.body(body).value.span;
534     for (_, node) in map.parent_iter(body.hir_id) {
535         match node {
536             Node::Expr(e) => span = e.span,
537             Node::Block(_) | Node::Arm(_) | Node::Stmt(_) | Node::Local(_) => (),
538             _ => break,
539         }
540     }
541     Some(span)
542 }
543
544 fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
545     let source_map = cx.sess().source_map();
546     let ctxt = span.ctxt();
547     if ctxt == SyntaxContext::root()
548         && let Some(search_span) = get_body_search_span(cx)
549     {
550         if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
551             && let Some(body_span) = walk_span_to_context(search_span, SyntaxContext::root())
552             && let Ok(body_line) = source_map.lookup_line(body_span.lo())
553             && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
554             && let Some(src) = unsafe_line.sf.src.as_deref()
555         {
556             // Get the text from the start of function body to the unsafe block.
557             //     fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
558             //              ^-------------^
559             unsafe_line.sf.lines(|lines| {
560                 body_line.line < unsafe_line.line && text_has_safety_comment(
561                     src,
562                     &lines[body_line.line + 1..=unsafe_line.line],
563                     unsafe_line.sf.start_pos.to_usize(),
564                 ).is_some()
565             })
566         } else {
567             // Problem getting source text. Pretend a comment was found.
568             true
569         }
570     } else {
571         false
572     }
573 }
574
575 /// Checks if the given text has a safety comment for the immediately proceeding line.
576 fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> Option<BytePos> {
577     let mut lines = line_starts
578         .array_windows::<2>()
579         .rev()
580         .map_while(|[start, end]| {
581             let start = start.to_usize() - offset;
582             let end = end.to_usize() - offset;
583             let text = src.get(start..end)?;
584             let trimmed = text.trim_start();
585             Some((start + (text.len() - trimmed.len()), trimmed))
586         })
587         .filter(|(_, text)| !text.is_empty());
588
589     let Some((line_start, line)) = lines.next() else {
590         return None;
591     };
592     // Check for a sequence of line comments.
593     if line.starts_with("//") {
594         let (mut line, mut line_start) = (line, line_start);
595         loop {
596             if line.to_ascii_uppercase().contains("SAFETY:") {
597                 return Some(BytePos(
598                     u32::try_from(line_start).unwrap() + u32::try_from(offset).unwrap(),
599                 ));
600             }
601             match lines.next() {
602                 Some((s, x)) if x.starts_with("//") => (line, line_start) = (x, s),
603                 _ => return None,
604             }
605         }
606     }
607     // No line comments; look for the start of a block comment.
608     // This will only find them if they are at the start of a line.
609     let (mut line_start, mut line) = (line_start, line);
610     loop {
611         if line.starts_with("/*") {
612             let src = &src[line_start..line_starts.last().unwrap().to_usize() - offset];
613             let mut tokens = tokenize(src);
614             return (src[..tokens.next().unwrap().len as usize]
615                 .to_ascii_uppercase()
616                 .contains("SAFETY:")
617                 && tokens.all(|t| t.kind == TokenKind::Whitespace))
618             .then_some(BytePos(
619                 u32::try_from(line_start).unwrap() + u32::try_from(offset).unwrap(),
620             ));
621         }
622         match lines.next() {
623             Some(x) => (line_start, line) = x,
624             None => return None,
625         }
626     }
627 }