]> git.proxmox.com Git - rustc.git/blob - src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs
New upstream version 1.69.0+dfsg1
[rustc.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 if cx.tcx.hir().parent_iter(expr.hir_id).any(|(_, ref node)| {
267 matches!(
268 node,
269 Node::Block(&Block {
270 rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided),
271 ..
272 }),
273 )
274 }) {
275 return None;
276 }
277
278 // this should roughly be the reverse of `block_parents_have_safety_comment`
279 if for_each_expr_with_closures(cx, expr, |expr| match expr.kind {
280 hir::ExprKind::Block(
281 Block {
282 rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided),
283 ..
284 },
285 _,
286 ) => ControlFlow::Break(()),
287 // statements will be handled by check_stmt itself again
288 hir::ExprKind::Block(..) => ControlFlow::Continue(Descend::No),
289 _ => ControlFlow::Continue(Descend::Yes),
290 })
291 .is_some()
292 {
293 return None;
294 }
295
296 let source_map = cx.tcx.sess.source_map();
297 let span = Span::new(comment_pos, comment_pos, SyntaxContext::root(), None);
298 let help_span = source_map.span_extend_to_next_char(span, '\n', true);
299
300 Some(help_span)
301 }
302
303 fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool {
304 let source_map = cx.sess().source_map();
305 let file_pos = source_map.lookup_byte_offset(span.lo());
306 file_pos
307 .sf
308 .src
309 .as_deref()
310 .and_then(|src| src.get(file_pos.pos.to_usize()..))
311 .map_or(true, |src| !src.starts_with("unsafe"))
312 }
313
314 // Checks if any parent {expression, statement, block, local, const, static}
315 // has a safety comment
316 fn block_parents_have_safety_comment(cx: &LateContext<'_>, id: hir::HirId) -> bool {
317 if let Some(node) = get_parent_node(cx.tcx, id) {
318 return match node {
319 Node::Expr(expr) => !is_branchy(expr) && span_in_body_has_safety_comment(cx, expr.span),
320 Node::Stmt(hir::Stmt {
321 kind:
322 hir::StmtKind::Local(hir::Local { span, .. })
323 | hir::StmtKind::Expr(hir::Expr { span, .. })
324 | hir::StmtKind::Semi(hir::Expr { span, .. }),
325 ..
326 })
327 | Node::Local(hir::Local { span, .. })
328 | Node::Item(hir::Item {
329 kind: hir::ItemKind::Const(..) | ItemKind::Static(..),
330 span,
331 ..
332 }) => span_in_body_has_safety_comment(cx, *span),
333 _ => false,
334 };
335 }
336 false
337 }
338
339 /// Checks if an expression is "branchy", e.g. loop, match/if/etc.
340 fn is_branchy(expr: &hir::Expr<'_>) -> bool {
341 matches!(
342 expr.kind,
343 hir::ExprKind::If(..) | hir::ExprKind::Loop(..) | hir::ExprKind::Match(..)
344 )
345 }
346
347 /// Checks if the lines immediately preceding the block contain a safety comment.
348 fn block_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
349 // This intentionally ignores text before the start of a function so something like:
350 // ```
351 // // SAFETY: reason
352 // fn foo() { unsafe { .. } }
353 // ```
354 // won't work. This is to avoid dealing with where such a comment should be place relative to
355 // attributes and doc comments.
356
357 matches!(
358 span_from_macro_expansion_has_safety_comment(cx, span),
359 HasSafetyComment::Yes(_)
360 ) || span_in_body_has_safety_comment(cx, span)
361 }
362
363 enum HasSafetyComment {
364 Yes(BytePos),
365 No,
366 Maybe,
367 }
368
369 /// Checks if the lines immediately preceding the item contain a safety comment.
370 #[allow(clippy::collapsible_match)]
371 fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> HasSafetyComment {
372 match span_from_macro_expansion_has_safety_comment(cx, item.span) {
373 HasSafetyComment::Maybe => (),
374 has_safety_comment => return has_safety_comment,
375 }
376
377 if item.span.ctxt() != SyntaxContext::root() {
378 return HasSafetyComment::No;
379 }
380 if let Some(parent_node) = get_parent_node(cx.tcx, item.hir_id()) {
381 let comment_start = match parent_node {
382 Node::Crate(parent_mod) => {
383 comment_start_before_item_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item)
384 },
385 Node::Item(parent_item) => {
386 if let ItemKind::Mod(parent_mod) = &parent_item.kind {
387 comment_start_before_item_in_mod(cx, parent_mod, parent_item.span, item)
388 } else {
389 // Doesn't support impls in this position. Pretend a comment was found.
390 return HasSafetyComment::Maybe;
391 }
392 },
393 Node::Stmt(stmt) => {
394 if let Some(Node::Block(block)) = get_parent_node(cx.tcx, stmt.hir_id) {
395 walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo)
396 } else {
397 // Problem getting the parent node. Pretend a comment was found.
398 return HasSafetyComment::Maybe;
399 }
400 },
401 _ => {
402 // Doesn't support impls in this position. Pretend a comment was found.
403 return HasSafetyComment::Maybe;
404 },
405 };
406
407 let source_map = cx.sess().source_map();
408 if let Some(comment_start) = comment_start
409 && let Ok(unsafe_line) = source_map.lookup_line(item.span.lo())
410 && let Ok(comment_start_line) = source_map.lookup_line(comment_start)
411 && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf)
412 && let Some(src) = unsafe_line.sf.src.as_deref()
413 {
414 return unsafe_line.sf.lines(|lines| {
415 if comment_start_line.line >= unsafe_line.line {
416 HasSafetyComment::No
417 } else {
418 match text_has_safety_comment(
419 src,
420 &lines[comment_start_line.line + 1..=unsafe_line.line],
421 unsafe_line.sf.start_pos.to_usize(),
422 ) {
423 Some(b) => HasSafetyComment::Yes(b),
424 None => HasSafetyComment::No,
425 }
426 }
427 });
428 }
429 }
430 HasSafetyComment::Maybe
431 }
432
433 /// Checks if the lines immediately preceding the item contain a safety comment.
434 #[allow(clippy::collapsible_match)]
435 fn stmt_has_safety_comment(cx: &LateContext<'_>, span: Span, hir_id: HirId) -> HasSafetyComment {
436 match span_from_macro_expansion_has_safety_comment(cx, span) {
437 HasSafetyComment::Maybe => (),
438 has_safety_comment => return has_safety_comment,
439 }
440
441 if span.ctxt() != SyntaxContext::root() {
442 return HasSafetyComment::No;
443 }
444
445 if let Some(parent_node) = get_parent_node(cx.tcx, hir_id) {
446 let comment_start = match parent_node {
447 Node::Block(block) => walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo),
448 _ => return HasSafetyComment::Maybe,
449 };
450
451 let source_map = cx.sess().source_map();
452 if let Some(comment_start) = comment_start
453 && let Ok(unsafe_line) = source_map.lookup_line(span.lo())
454 && let Ok(comment_start_line) = source_map.lookup_line(comment_start)
455 && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf)
456 && let Some(src) = unsafe_line.sf.src.as_deref()
457 {
458 return unsafe_line.sf.lines(|lines| {
459 if comment_start_line.line >= unsafe_line.line {
460 HasSafetyComment::No
461 } else {
462 match text_has_safety_comment(
463 src,
464 &lines[comment_start_line.line + 1..=unsafe_line.line],
465 unsafe_line.sf.start_pos.to_usize(),
466 ) {
467 Some(b) => HasSafetyComment::Yes(b),
468 None => HasSafetyComment::No,
469 }
470 }
471 });
472 }
473 }
474 HasSafetyComment::Maybe
475 }
476
477 fn comment_start_before_item_in_mod(
478 cx: &LateContext<'_>,
479 parent_mod: &hir::Mod<'_>,
480 parent_mod_span: Span,
481 item: &hir::Item<'_>,
482 ) -> Option<BytePos> {
483 parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| {
484 if *item_id == item.item_id() {
485 if idx == 0 {
486 // mod A { /* comment */ unsafe impl T {} ... }
487 // ^------------------------------------------^ returns the start of this span
488 // ^---------------------^ finally checks comments in this range
489 if let Some(sp) = walk_span_to_context(parent_mod_span, SyntaxContext::root()) {
490 return Some(sp.lo());
491 }
492 } else {
493 // some_item /* comment */ unsafe impl T {}
494 // ^-------^ returns the end of this span
495 // ^---------------^ finally checks comments in this range
496 let prev_item = cx.tcx.hir().item(parent_mod.item_ids[idx - 1]);
497 if let Some(sp) = walk_span_to_context(prev_item.span, SyntaxContext::root()) {
498 return Some(sp.hi());
499 }
500 }
501 }
502 None
503 })
504 }
505
506 fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> HasSafetyComment {
507 let source_map = cx.sess().source_map();
508 let ctxt = span.ctxt();
509 if ctxt == SyntaxContext::root() {
510 HasSafetyComment::Maybe
511 } else {
512 // From a macro expansion. Get the text from the start of the macro declaration to start of the
513 // unsafe block.
514 // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
515 // ^--------------------------------------------^
516 if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
517 && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
518 && Lrc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
519 && let Some(src) = unsafe_line.sf.src.as_deref()
520 {
521 unsafe_line.sf.lines(|lines| {
522 if macro_line.line < unsafe_line.line {
523 match text_has_safety_comment(
524 src,
525 &lines[macro_line.line + 1..=unsafe_line.line],
526 unsafe_line.sf.start_pos.to_usize(),
527 ) {
528 Some(b) => HasSafetyComment::Yes(b),
529 None => HasSafetyComment::No,
530 }
531 } else {
532 HasSafetyComment::No
533 }
534 })
535 } else {
536 // Problem getting source text. Pretend a comment was found.
537 HasSafetyComment::Maybe
538 }
539 }
540 }
541
542 fn get_body_search_span(cx: &LateContext<'_>) -> Option<Span> {
543 let body = cx.enclosing_body?;
544 let map = cx.tcx.hir();
545 let mut span = map.body(body).value.span;
546 for (_, node) in map.parent_iter(body.hir_id) {
547 match node {
548 Node::Expr(e) => span = e.span,
549 Node::Block(_) | Node::Arm(_) | Node::Stmt(_) | Node::Local(_) => (),
550 _ => break,
551 }
552 }
553 Some(span)
554 }
555
556 fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool {
557 let source_map = cx.sess().source_map();
558 let ctxt = span.ctxt();
559 if ctxt == SyntaxContext::root()
560 && let Some(search_span) = get_body_search_span(cx)
561 {
562 if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
563 && let Some(body_span) = walk_span_to_context(search_span, SyntaxContext::root())
564 && let Ok(body_line) = source_map.lookup_line(body_span.lo())
565 && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf)
566 && let Some(src) = unsafe_line.sf.src.as_deref()
567 {
568 // Get the text from the start of function body to the unsafe block.
569 // fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
570 // ^-------------^
571 unsafe_line.sf.lines(|lines| {
572 body_line.line < unsafe_line.line && text_has_safety_comment(
573 src,
574 &lines[body_line.line + 1..=unsafe_line.line],
575 unsafe_line.sf.start_pos.to_usize(),
576 ).is_some()
577 })
578 } else {
579 // Problem getting source text. Pretend a comment was found.
580 true
581 }
582 } else {
583 false
584 }
585 }
586
587 /// Checks if the given text has a safety comment for the immediately proceeding line.
588 fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> Option<BytePos> {
589 let mut lines = line_starts
590 .array_windows::<2>()
591 .rev()
592 .map_while(|[start, end]| {
593 let start = start.to_usize() - offset;
594 let end = end.to_usize() - offset;
595 let text = src.get(start..end)?;
596 let trimmed = text.trim_start();
597 Some((start + (text.len() - trimmed.len()), trimmed))
598 })
599 .filter(|(_, text)| !text.is_empty());
600
601 let Some((line_start, line)) = lines.next() else {
602 return None;
603 };
604 // Check for a sequence of line comments.
605 if line.starts_with("//") {
606 let (mut line, mut line_start) = (line, line_start);
607 loop {
608 if line.to_ascii_uppercase().contains("SAFETY:") {
609 return Some(BytePos(
610 u32::try_from(line_start).unwrap() + u32::try_from(offset).unwrap(),
611 ));
612 }
613 match lines.next() {
614 Some((s, x)) if x.starts_with("//") => (line, line_start) = (x, s),
615 _ => return None,
616 }
617 }
618 }
619 // No line comments; look for the start of a block comment.
620 // This will only find them if they are at the start of a line.
621 let (mut line_start, mut line) = (line_start, line);
622 loop {
623 if line.starts_with("/*") {
624 let src = &src[line_start..line_starts.last().unwrap().to_usize() - offset];
625 let mut tokens = tokenize(src);
626 return (src[..tokens.next().unwrap().len as usize]
627 .to_ascii_uppercase()
628 .contains("SAFETY:")
629 && tokens.all(|t| t.kind == TokenKind::Whitespace))
630 .then_some(BytePos(
631 u32::try_from(line_start).unwrap() + u32::try_from(offset).unwrap(),
632 ));
633 }
634 match lines.next() {
635 Some(x) => (line_start, line) = x,
636 None => return None,
637 }
638 }
639 }