use std::ops::ControlFlow;
use std::sync::Arc;

use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::is_lint_allowed;
use clippy_utils::source::walk_span_to_context;
use clippy_utils::visitors::{Descend, for_each_expr};
use hir::HirId;
use rustc_errors::Applicability;
use rustc_hir::{self as hir, Block, BlockCheckMode, FnSig, Impl, ItemKind, Node, UnsafeSource};
use rustc_lexer::{FrontmatterAllowed, TokenKind, tokenize};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_session::impl_lint_pass;
use rustc_span::{BytePos, Pos, RelativeBytePos, Span, SyntaxContext};

declare_clippy_lint! {
    /// ### What it does
    /// Checks for `unsafe` blocks and impls without a `// SAFETY: ` comment
    /// explaining why the unsafe operations performed inside
    /// the block are safe.
    ///
    /// Note the comment must appear on the line(s) preceding the unsafe block
    /// with nothing appearing in between. The following is ok:
    /// ```ignore
    /// foo(
    ///     // SAFETY:
    ///     // This is a valid safety comment
    ///     unsafe { *x }
    /// )
    /// ```
    /// But neither of these are:
    /// ```ignore
    /// // SAFETY:
    /// // This is not a valid safety comment
    /// foo(
    ///     /* SAFETY: Neither is this */ unsafe { *x },
    /// );
    /// ```
    ///
    /// ### Why restrict this?
    /// Undocumented unsafe blocks and impls can make it difficult to read and maintain code.
    /// Writing out the safety justification may help in discovering unsoundness or bugs.
    ///
    /// ### Example
    /// ```no_run
    /// use std::ptr::NonNull;
    /// let a = &mut 42;
    ///
    /// let ptr = unsafe { NonNull::new_unchecked(a) };
    /// ```
    /// Use instead:
    /// ```no_run
    /// use std::ptr::NonNull;
    /// let a = &mut 42;
    ///
    /// // SAFETY: references are guaranteed to be non-null.
    /// let ptr = unsafe { NonNull::new_unchecked(a) };
    /// ```
    #[clippy::version = "1.58.0"]
    pub UNDOCUMENTED_UNSAFE_BLOCKS,
    restriction,
    "creating an unsafe block without explaining why it is safe"
}
declare_clippy_lint! {
    /// ### What it does
    /// Checks for `// SAFETY: ` comments on safe code.
    ///
    /// ### Why restrict this?
    /// Safe code has no safety requirements, so there is no need to
    /// describe safety invariants.
    ///
    /// ### Example
    /// ```no_run
    /// use std::ptr::NonNull;
    /// let a = &mut 42;
    ///
    /// // SAFETY: references are guaranteed to be non-null.
    /// let ptr = NonNull::new(a).unwrap();
    /// ```
    /// Use instead:
    /// ```no_run
    /// use std::ptr::NonNull;
    /// let a = &mut 42;
    ///
    /// let ptr = NonNull::new(a).unwrap();
    /// ```
    #[clippy::version = "1.67.0"]
    pub UNNECESSARY_SAFETY_COMMENT,
    restriction,
    "annotating safe code with a safety comment"
}

pub struct UndocumentedUnsafeBlocks {
    accept_comment_above_statement: bool,
    accept_comment_above_attributes: bool,
}

impl UndocumentedUnsafeBlocks {
    pub fn new(conf: &'static Conf) -> Self {
        Self {
            accept_comment_above_statement: conf.accept_comment_above_statement,
            accept_comment_above_attributes: conf.accept_comment_above_attributes,
        }
    }
}

impl_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS, UNNECESSARY_SAFETY_COMMENT]);

impl<'tcx> LateLintPass<'tcx> for UndocumentedUnsafeBlocks {
    fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) {
        if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
            && !block.span.in_external_macro(cx.tcx.sess.source_map())
            && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id)
            && !is_unsafe_from_proc_macro(cx, block.span)
            && !block_has_safety_comment(cx, block.span, self.accept_comment_above_attributes)
            && !block_parents_have_safety_comment(
                self.accept_comment_above_statement,
                self.accept_comment_above_attributes,
                cx,
                block.hir_id,
            )
        {
            let source_map = cx.tcx.sess.source_map();
            let span = if source_map.is_multiline(block.span) {
                source_map.span_until_char(block.span, '\n')
            } else {
                block.span
            };

            #[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")]
            span_lint_and_then(
                cx,
                UNDOCUMENTED_UNSAFE_BLOCKS,
                span,
                "unsafe block missing a safety comment",
                |diag| {
                    diag.help("consider adding a safety comment on the preceding line");
                },
            );
        }

        if let Some(tail) = block.expr
            && !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, tail.hir_id)
            && !tail.span.in_external_macro(cx.tcx.sess.source_map())
            && let HasSafetyComment::Yes(pos, _) =
                stmt_has_safety_comment(cx, tail.span, tail.hir_id, self.accept_comment_above_attributes)
            && let Some(help_span) = expr_has_unnecessary_safety_comment(cx, tail, pos)
        {
            span_lint_and_then(
                cx,
                UNNECESSARY_SAFETY_COMMENT,
                tail.span,
                "expression has unnecessary safety comment",
                |diag| {
                    diag.span_help(help_span, "consider removing the safety comment");
                },
            );
        }
    }

    fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &hir::Stmt<'tcx>) {
        let (hir::StmtKind::Let(&hir::LetStmt { init: Some(expr), .. })
        | hir::StmtKind::Expr(expr)
        | hir::StmtKind::Semi(expr)) = stmt.kind
        else {
            return;
        };
        if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, stmt.hir_id)
            && !stmt.span.in_external_macro(cx.tcx.sess.source_map())
            && let HasSafetyComment::Yes(pos, _) =
                stmt_has_safety_comment(cx, stmt.span, stmt.hir_id, self.accept_comment_above_attributes)
            && let Some(help_span) = expr_has_unnecessary_safety_comment(cx, expr, pos)
        {
            span_lint_and_then(
                cx,
                UNNECESSARY_SAFETY_COMMENT,
                stmt.span,
                "statement has unnecessary safety comment",
                |diag| {
                    diag.span_help(help_span, "consider removing the safety comment");
                },
            );
        }
    }

    fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) {
        if item.span.in_external_macro(cx.tcx.sess.source_map()) {
            return;
        }

        let mk_spans = |pos: BytePos| {
            let source_map = cx.tcx.sess.source_map();
            let help_span = Span::new(
                pos,
                pos + BytePos(u32::try_from("SAFETY:".len()).unwrap()),
                SyntaxContext::root(),
                None,
            );
            let span = if source_map.is_multiline(item.span) {
                source_map.span_until_char(item.span, '\n')
            } else {
                item.span
            };
            (span, help_span)
        };

        let item_has_safety_comment = item_has_safety_comment(cx, item, self.accept_comment_above_attributes);
        match item_has_safety_comment {
            HasSafetyComment::Yes(pos, is_doc) => check_has_safety_comment(cx, item, mk_spans(pos), is_doc),
            HasSafetyComment::No => check_has_no_safety_comment(cx, item),
            HasSafetyComment::Maybe => {},
        }
    }
}

fn check_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>, (span, help_span): (Span, Span), is_doc: bool) {
    match &item.kind {
        ItemKind::Impl(Impl {
            of_trait: Some(of_trait),
            ..
        }) if of_trait.safety.is_safe() => {
            if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) {
                span_lint_and_then(
                    cx,
                    UNNECESSARY_SAFETY_COMMENT,
                    span,
                    "impl has unnecessary safety comment",
                    |diag| {
                        diag.span_help(help_span, "consider removing the safety comment");
                    },
                );
            }
        },
        ItemKind::Impl(_) => {},
        // const and static items only need a safety comment if their body is an unsafe block, lint otherwise
        &ItemKind::Const(.., body) | &ItemKind::Static(.., body) => {
            if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, body.hir_id) {
                let body = cx.tcx.hir_body(body);
                if !matches!(
                    body.value.kind, hir::ExprKind::Block(block, _)
                    if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided)
                ) {
                    span_lint_and_then(
                        cx,
                        UNNECESSARY_SAFETY_COMMENT,
                        span,
                        format!(
                            "{} has unnecessary safety comment",
                            cx.tcx.def_descr(item.owner_id.to_def_id()),
                        ),
                        |diag| {
                            diag.span_help(help_span, "consider removing the safety comment");
                        },
                    );
                }
            }
        },
        // Unsafe functions with a SAFETY comment are suggested to change it to a `# Safety` comment
        ItemKind::Fn {
            sig: FnSig { header, .. },
            ..
        } if header.is_unsafe() => {
            if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) {
                span_lint_and_then(
                    cx,
                    UNNECESSARY_SAFETY_COMMENT,
                    span,
                    format!(
                        "{} has unnecessary safety comment",
                        cx.tcx.def_descr(item.owner_id.to_def_id()),
                    ),
                    |diag| {
                        if is_doc {
                            // If it's already within a doc comment, we try to suggest the change

                            diag.span_suggestion(
                                help_span,
                                "consider changing it to a `# Safety` section",
                                "# Safety",
                                Applicability::MachineApplicable,
                            );
                        } else {
                            diag.span_help(
                                help_span,
                                "consider changing the `safety` comment for a `# Safety` doc comment",
                            );
                        }
                    },
                );
            }
        },
        // Aside from unsafe impls and consts/statics with an unsafe block, items in general
        // do not have safety invariants that need to be documented, so lint those.
        _ => {
            if !is_lint_allowed(cx, UNNECESSARY_SAFETY_COMMENT, item.hir_id()) {
                span_lint_and_then(
                    cx,
                    UNNECESSARY_SAFETY_COMMENT,
                    span,
                    format!(
                        "{} has unnecessary safety comment",
                        cx.tcx.def_descr(item.owner_id.to_def_id()),
                    ),
                    |diag| {
                        diag.span_help(help_span, "consider removing the safety comment");
                    },
                );
            }
        },
    }
}

fn check_has_no_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) {
    if let ItemKind::Impl(Impl {
        of_trait: Some(of_trait),
        ..
    }) = item.kind
        && of_trait.safety.is_unsafe()
        && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id())
        && !is_unsafe_from_proc_macro(cx, item.span)
    {
        let source_map = cx.tcx.sess.source_map();
        let span = if source_map.is_multiline(item.span) {
            source_map.span_until_char(item.span, '\n')
        } else {
            item.span
        };

        #[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")]
        span_lint_and_then(
            cx,
            UNDOCUMENTED_UNSAFE_BLOCKS,
            span,
            "unsafe impl missing a safety comment",
            |diag| {
                diag.help("consider adding a safety comment on the preceding line");
            },
        );
    }
}
fn expr_has_unnecessary_safety_comment<'tcx>(
    cx: &LateContext<'tcx>,
    expr: &'tcx hir::Expr<'tcx>,
    comment_pos: BytePos,
) -> Option<Span> {
    if cx.tcx.hir_parent_iter(expr.hir_id).any(|(_, ref node)| {
        matches!(
            node,
            Node::Block(Block {
                rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided),
                ..
            }),
        )
    }) {
        return None;
    }

    // this should roughly be the reverse of `block_parents_have_safety_comment`
    if for_each_expr(cx, expr, |expr| match expr.kind {
        hir::ExprKind::Block(
            Block {
                rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided),
                ..
            },
            _,
        ) => ControlFlow::Break(()),
        // `_ = foo()` is desugared to `{ let _ = foo(); }`
        hir::ExprKind::Block(
            Block {
                rules: BlockCheckMode::DefaultBlock,
                stmts:
                    [
                        hir::Stmt {
                            kind:
                                hir::StmtKind::Let(hir::LetStmt {
                                    source: hir::LocalSource::AssignDesugar(_),
                                    ..
                                }),
                            ..
                        },
                    ],
                ..
            },
            _,
        ) => ControlFlow::Continue(Descend::Yes),
        // statements will be handled by check_stmt itself again
        hir::ExprKind::Block(..) => ControlFlow::Continue(Descend::No),
        _ => ControlFlow::Continue(Descend::Yes),
    })
    .is_some()
    {
        return None;
    }

    let source_map = cx.tcx.sess.source_map();
    let span = Span::new(comment_pos, comment_pos, SyntaxContext::root(), None);
    let help_span = source_map.span_extend_to_next_char(span, '\n', true);

    Some(help_span)
}

fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool {
    let source_map = cx.sess().source_map();
    let file_pos = source_map.lookup_byte_offset(span.lo());
    file_pos
        .sf
        .src
        .as_deref()
        .and_then(|src| src.get(file_pos.pos.to_usize()..))
        .is_none_or(|src| !src.starts_with("unsafe"))
}

fn find_unsafe_block_parent_in_expr<'tcx>(
    cx: &LateContext<'tcx>,
    expr: &'tcx hir::Expr<'tcx>,
) -> Option<(Span, HirId)> {
    match cx.tcx.parent_hir_node(expr.hir_id) {
        Node::LetStmt(hir::LetStmt { span, hir_id, .. })
        | Node::Expr(hir::Expr {
            hir_id,
            kind: hir::ExprKind::Assign(_, _, span),
            ..
        }) => Some((*span, *hir_id)),
        Node::Expr(expr) => find_unsafe_block_parent_in_expr(cx, expr),
        node if let Some((span, hir_id)) = span_and_hid_of_item_alike_node(&node)
            && is_const_or_static(&node) =>
        {
            Some((span, hir_id))
        },

        _ => {
            if is_branchy(expr) {
                return None;
            }
            Some((expr.span, expr.hir_id))
        },
    }
}

// Checks if any parent {expression, statement, block, local, const, static}
// has a safety comment
fn block_parents_have_safety_comment(
    accept_comment_above_statement: bool,
    accept_comment_above_attributes: bool,
    cx: &LateContext<'_>,
    id: HirId,
) -> bool {
    let span = match cx.tcx.parent_hir_node(id) {
        Node::Expr(expr) if let Some((span, _)) = find_unsafe_block_parent_in_expr(cx, expr) => span,
        Node::Stmt(hir::Stmt {
            kind:
                hir::StmtKind::Let(hir::LetStmt { span, .. })
                | hir::StmtKind::Expr(hir::Expr { span, .. })
                | hir::StmtKind::Semi(hir::Expr { span, .. }),
            ..
        })
        | Node::LetStmt(hir::LetStmt { span, .. }) => *span,

        node if let Some((span, _)) = span_and_hid_of_item_alike_node(&node)
            && is_const_or_static(&node) =>
        {
            span
        },

        _ => return false,
    };
    // if unsafe block is part of a let/const/static statement,
    // and accept_comment_above_statement is set to true
    // we accept the safety comment in the line the precedes this statement.
    accept_comment_above_statement && span_has_safety_comment(cx, span, accept_comment_above_attributes)
}

/// Checks if an expression is "branchy", e.g. loop, match/if/etc.
fn is_branchy(expr: &hir::Expr<'_>) -> bool {
    matches!(
        expr.kind,
        hir::ExprKind::If(..) | hir::ExprKind::Loop(..) | hir::ExprKind::Match(..)
    )
}

/// Checks if the lines immediately preceding the block contain a safety comment.
fn block_has_safety_comment(cx: &LateContext<'_>, span: Span, accept_comment_above_attributes: bool) -> bool {
    // This intentionally ignores text before the start of a function so something like:
    // ```
    //     // SAFETY: reason
    //     fn foo() { unsafe { .. } }
    // ```
    // won't work. This is to avoid dealing with where such a comment should be place relative to
    // attributes and doc comments.

    matches!(
        span_from_macro_expansion_has_safety_comment(cx, span, accept_comment_above_attributes),
        HasSafetyComment::Yes(_, _)
    ) || span_has_safety_comment(cx, span, accept_comment_above_attributes)
}

#[derive(Debug)]
enum HasSafetyComment {
    Yes(BytePos, bool),
    No,
    Maybe,
}

/// Checks if the lines immediately preceding the item contain a safety comment.
fn item_has_safety_comment(
    cx: &LateContext<'_>,
    item: &hir::Item<'_>,
    accept_comment_above_attributes: bool,
) -> HasSafetyComment {
    match span_from_macro_expansion_has_safety_comment(cx, item.span, accept_comment_above_attributes) {
        HasSafetyComment::Maybe => (),
        has_safety_comment => return has_safety_comment,
    }

    if item.span.ctxt() != SyntaxContext::root() {
        return HasSafetyComment::No;
    }
    let comment_start = match cx.tcx.parent_hir_node(item.hir_id()) {
        Node::Crate(parent_mod) => comment_start_before_item_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item),
        Node::Item(parent_item) => {
            if let ItemKind::Mod(_, parent_mod) = &parent_item.kind {
                comment_start_before_item_in_mod(cx, parent_mod, parent_item.span, item)
            } else {
                // Doesn't support impls in this position. Pretend a comment was found.
                return HasSafetyComment::Maybe;
            }
        },
        Node::Stmt(stmt) => {
            if let Node::Block(block) = cx.tcx.parent_hir_node(stmt.hir_id) {
                walk_span_to_context(block.span, SyntaxContext::root())
                    .map(|sp| CommentStartBeforeItem::Offset(sp.lo()))
            } else {
                // Problem getting the parent node. Pretend a comment was found.
                return HasSafetyComment::Maybe;
            }
        },
        _ => {
            // Doesn't support impls in this position. Pretend a comment was found.
            return HasSafetyComment::Maybe;
        },
    };

    let source_map = cx.sess().source_map();
    // If the comment is in the first line of the file, there is no preceding line
    if let Some(comment_start) = comment_start
        && let Ok(unsafe_line) = source_map.lookup_line(item.span.lo())
        && let Ok(comment_start_line) = source_map.lookup_line(comment_start.into())
        && let include_first_line_of_file = matches!(comment_start, CommentStartBeforeItem::Start)
        && (include_first_line_of_file || Arc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf))
        && let Some(src) = unsafe_line.sf.src.as_deref()
    {
        return if comment_start_line.line >= unsafe_line.line {
            HasSafetyComment::No
        } else {
            text_has_safety_comment(
                src,
                &unsafe_line.sf.lines()
                    [(comment_start_line.line + usize::from(!include_first_line_of_file))..=unsafe_line.line],
                unsafe_line.sf.start_pos,
                accept_comment_above_attributes,
            )
        };
    }
    HasSafetyComment::Maybe
}

/// Checks if the lines immediately preceding the item contain a safety comment.
fn stmt_has_safety_comment(
    cx: &LateContext<'_>,
    span: Span,
    hir_id: HirId,
    accept_comment_above_attributes: bool,
) -> HasSafetyComment {
    match span_from_macro_expansion_has_safety_comment(cx, span, accept_comment_above_attributes) {
        HasSafetyComment::Maybe => (),
        has_safety_comment => return has_safety_comment,
    }

    if span.ctxt() != SyntaxContext::root() {
        return HasSafetyComment::No;
    }

    let comment_start = match cx.tcx.parent_hir_node(hir_id) {
        Node::Block(block) => walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo),
        _ => return HasSafetyComment::Maybe,
    };

    let source_map = cx.sess().source_map();
    if let Some(comment_start) = comment_start
        && let Ok(unsafe_line) = source_map.lookup_line(span.lo())
        && let Ok(comment_start_line) = source_map.lookup_line(comment_start)
        && Arc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf)
        && let Some(src) = unsafe_line.sf.src.as_deref()
    {
        return if comment_start_line.line >= unsafe_line.line {
            HasSafetyComment::No
        } else {
            text_has_safety_comment(
                src,
                &unsafe_line.sf.lines()[comment_start_line.line + 1..=unsafe_line.line],
                unsafe_line.sf.start_pos,
                accept_comment_above_attributes,
            )
        };
    }
    HasSafetyComment::Maybe
}

#[derive(Clone, Copy, Debug)]
enum CommentStartBeforeItem {
    Offset(BytePos),
    Start,
}

impl From<CommentStartBeforeItem> for BytePos {
    fn from(value: CommentStartBeforeItem) -> Self {
        match value {
            CommentStartBeforeItem::Offset(loc) => loc,
            CommentStartBeforeItem::Start => BytePos(0),
        }
    }
}

fn comment_start_before_item_in_mod(
    cx: &LateContext<'_>,
    parent_mod: &hir::Mod<'_>,
    parent_mod_span: Span,
    item: &hir::Item<'_>,
) -> Option<CommentStartBeforeItem> {
    parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| {
        if *item_id == item.item_id() {
            if idx == 0 {
                // mod A { /* comment */ unsafe impl T {} ... }
                // ^------------------------------------------^ returns the start of this span
                // ^---------------------^ finally checks comments in this range
                if let Some(sp) = walk_span_to_context(parent_mod_span, SyntaxContext::root()) {
                    return Some(CommentStartBeforeItem::Offset(sp.lo()));
                }
            } else {
                // some_item /* comment */ unsafe impl T {}
                // ^-------^ returns the end of this span
                //         ^---------------^ finally checks comments in this range
                let prev_item = cx.tcx.hir_item(parent_mod.item_ids[idx - 1]);
                if prev_item.span.is_dummy() {
                    return Some(CommentStartBeforeItem::Start);
                }
                if let Some(sp) = walk_span_to_context(prev_item.span, SyntaxContext::root()) {
                    return Some(CommentStartBeforeItem::Offset(sp.hi()));
                }
            }
        }
        None
    })
}

fn span_from_macro_expansion_has_safety_comment(
    cx: &LateContext<'_>,
    span: Span,
    accept_comment_above_attributes: bool,
) -> HasSafetyComment {
    let source_map = cx.sess().source_map();
    let ctxt = span.ctxt();
    if ctxt == SyntaxContext::root() {
        HasSafetyComment::Maybe
    }
    // From a macro expansion. Get the text from the start of the macro declaration to start of the
    // unsafe block.
    //     macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
    //     ^--------------------------------------------^
    else if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
        && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo())
        && Arc::ptr_eq(&unsafe_line.sf, &macro_line.sf)
        && let Some(src) = unsafe_line.sf.src.as_deref()
    {
        if macro_line.line < unsafe_line.line {
            text_has_safety_comment(
                src,
                &unsafe_line.sf.lines()[macro_line.line + 1..=unsafe_line.line],
                unsafe_line.sf.start_pos,
                accept_comment_above_attributes,
            )
        } else {
            HasSafetyComment::No
        }
    } else {
        // Problem getting source text. Pretend a comment was found.
        HasSafetyComment::Maybe
    }
}

fn get_body_search_span(cx: &LateContext<'_>) -> Option<Span> {
    let body = cx.enclosing_body?;
    let mut maybe_mod_item = None;

    for (_, parent_node) in cx.tcx.hir_parent_iter(body.hir_id) {
        match parent_node {
            Node::Crate(mod_) => return Some(mod_.spans.inner_span),
            Node::Item(hir::Item {
                kind: ItemKind::Mod(_, mod_),
                span,
                ..
            }) => {
                return maybe_mod_item
                    .and_then(|item| comment_start_before_item_in_mod(cx, mod_, *span, &item))
                    .map(|comment_start| mod_.spans.inner_span.with_lo(comment_start.into()))
                    .or(Some(*span));
            },
            node if let Some((span, _)) = span_and_hid_of_item_alike_node(&node)
                && !is_const_or_static(&node) =>
            {
                return Some(span);
            },
            Node::Item(item) => {
                maybe_mod_item = Some(*item);
            },
            _ => {
                maybe_mod_item = None;
            },
        }
    }
    None
}

fn span_has_safety_comment(cx: &LateContext<'_>, span: Span, accept_comment_above_attributes: bool) -> bool {
    let source_map = cx.sess().source_map();
    let ctxt = span.ctxt();
    if ctxt.is_root()
        && let Some(search_span) = get_body_search_span(cx)
    {
        if let Ok(unsafe_line) = source_map.lookup_line(span.lo())
            && let Some(body_span) = walk_span_to_context(search_span, SyntaxContext::root())
            && let Ok(body_line) = source_map.lookup_line(body_span.lo())
            && Arc::ptr_eq(&unsafe_line.sf, &body_line.sf)
            && let Some(src) = unsafe_line.sf.src.as_deref()
        {
            // Get the text from the start of function body to the unsafe block.
            //     fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
            //              ^-------------^
            body_line.line < unsafe_line.line
                && matches!(
                    text_has_safety_comment(
                        src,
                        &unsafe_line.sf.lines()[body_line.line + 1..=unsafe_line.line],
                        unsafe_line.sf.start_pos,
                        accept_comment_above_attributes,
                    ),
                    HasSafetyComment::Yes(..)
                )
        } else {
            // Problem getting source text. Pretend a comment was found.
            true
        }
    } else {
        false
    }
}

/// Checks if the given text has a safety comment for the immediately proceeding line.
///
/// If `accept_comment_above_attributes` is true, it will ignore attributes inbetween blocks of
/// comments
fn text_has_safety_comment(
    src: &str,
    line_starts: &[RelativeBytePos],
    start_pos: BytePos,
    accept_comment_above_attributes: bool,
) -> HasSafetyComment {
    let mut lines = line_starts
        .array_windows::<2>()
        .rev()
        .map_while(|[start, end]| {
            let start = start.to_usize();
            let end = end.to_usize();
            let text = src.get(start..end)?;
            let trimmed = text.trim_start();
            Some((start + (text.len() - trimmed.len()), trimmed))
        })
        .filter(|(_, text)| !(text.is_empty() || (accept_comment_above_attributes && is_attribute(text))));

    let Some((line_start, line)) = lines.next() else {
        return HasSafetyComment::No;
    };

    let mut in_codeblock = false;
    // Check for a sequence of line comments.
    if line.starts_with("//") {
        let (mut line, mut line_start) = (line, line_start);
        loop {
            // Don't lint if the safety comment is part of a codeblock in a doc comment.
            // It may or may not be required, and we can't very easily check it (and we shouldn't, since
            // the safety comment isn't referring to the node we're currently checking)
            if line.trim_start_matches("///").trim_start().starts_with("```") {
                in_codeblock = !in_codeblock;
            }

            if !in_codeblock && let Some(safety_pos) = line.to_ascii_uppercase().find("SAFETY:") {
                return HasSafetyComment::Yes(
                    start_pos
                        + BytePos(u32::try_from(line_start).unwrap())
                        + BytePos(u32::try_from(safety_pos).unwrap()),
                    line.starts_with("///"),
                );
            }
            match lines.next() {
                Some((s, x)) if x.starts_with("//") => (line, line_start) = (x, s),
                _ => return HasSafetyComment::No,
            }
        }
    }
    // No line comments; look for the start of a block comment.
    // This will only find them if they are at the start of a line.
    let (mut line_start, mut line) = (line_start, line);
    loop {
        if line.starts_with("/*") {
            let src = &src[line_start..line_starts.last().unwrap().to_usize()];
            let mut tokens = tokenize(src, FrontmatterAllowed::No);
            let a = tokens.next();
            if let Some(safety_pos) = src[..a.unwrap().len as usize].to_ascii_uppercase().find("SAFETY:")
                && tokens.all(|t| t.kind == TokenKind::Whitespace)
            {
                return HasSafetyComment::Yes(
                    start_pos
                        + BytePos(u32::try_from(line_start).unwrap())
                        + BytePos(u32::try_from(safety_pos).unwrap()),
                    line.starts_with("/**"),
                );
            }
            return HasSafetyComment::No;
        }
        match lines.next() {
            Some(x) => (line_start, line) = x,
            None => return HasSafetyComment::No,
        }
    }
}

fn is_attribute(text: &str) -> bool {
    (text.starts_with("#[") || text.starts_with("#![")) && text.trim_end().ends_with(']')
}

fn span_and_hid_of_item_alike_node(node: &Node<'_>) -> Option<(Span, HirId)> {
    match node {
        Node::Item(item) => Some((item.span, item.owner_id.into())),
        Node::TraitItem(ti) => Some((ti.span, ti.owner_id.into())),
        Node::ImplItem(ii) => Some((ii.span, ii.owner_id.into())),
        _ => None,
    }
}

fn is_const_or_static(node: &Node<'_>) -> bool {
    matches!(
        node,
        Node::Item(hir::Item {
            kind: ItemKind::Const(..) | ItemKind::Static(..),
            ..
        }) | Node::ImplItem(hir::ImplItem {
            kind: hir::ImplItemKind::Const(..),
            ..
        }) | Node::TraitItem(hir::TraitItem {
            kind: hir::TraitItemKind::Const(..),
            ..
        })
    )
}
